You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is the bug I promised to report after #16280 was fixed.
What is going wrong?
Curl fails to send window size updates when large differences exist in HTTP/2 stream throughput when using pauses.
This then presents as the transfer getting deadlocked, since Curl fails to request more data with more window size.
Symptoms
The first transfer finishes, the second transfer has a bunch of bytes buffered up after its initial window size, and the receiver is slowly drip-feeding them out.
The streams have received the following approximate amounts of data at time of deadlock:
(rr) p nix::recvd1
$1 = 104860013
(rr) p nix::recvd2
$2 = 10485760
I have captured a packet capture of the problem with SSLKEYLOGFILE and tcpdump and observed that there are numerous WINDOW_UPDATE frames for stream 1 and only one (at the very beginning) for stream 3 (the slow one).
That is to say, stream 3 has more data to read from the server, but curl didn't ask the server for it and is sitting twiddling its thumbs hoping someone, anyone, will give it some data.
Reproducer
This is a reproducer using a hacked up version of the Lix HTTP library.
There are two streams being fetched via the same connection, the latter of which is much much slower to read.
They are both fetching the same 100MB file of urandom garbage.
The data being fetched is zstd encoded (unsure if this matters; curl compression support is disabled for testing purposes).
auto ft = makeCurlFileTransfer2(0);
auto a = ft->download("https://localhost:9999/foo");
auto b = ft->download("https://localhost:9999/bar");
auto af = std::async(std::launch::async, [&] {
char c[1024];
for (;;) {
recvd1 += a->read(c, sizeof(c));
}
});
auto bf = std::async(std::launch::async, [&] {
char c[1024];
for (;;) {
recvd2 += b->read(c, sizeof(c));
usleep(1000);
}
});
return0;
For the runnable version, see repro.cc and Makefile; it has been checked to build with Clang 18 on NixOS.
I am deeply sorry about submitting 600 lines of C++ of repro.
I cut down the lines by about 50% over the original code, and it is now self-contained.
More reduction would take rewriting it and that would probably make the code harder to deal with for debugging.
If you need more reduction, I can do it, but it will take a while.
Set up the Caddy (2.8.4 was used) server as above (mkdir trash, dd some randomness), then run it:
caddy run --config Caddyfile
Run the repro:
$ make repro
$ SSLKEYLOGFILE=keys.log LD_PRELOAD=/path/to/curl/lib/libcurl.so CURL_DEBUG=all ./repro
If you don't want to copy paste this stuff, you can git clone https://gist.github.com/lf-/276cb01858d894f4946787f69254a923 repro/
Here is the way to obtain an approximately byte-identical Curl that I used using Nix, if necessary: put this in a file, then nix-build file.nix -A all. The results will be in result* symlinks. If you have to debug a Nix build of Curl, use NIX_DEBUG_INFO_DIRS=result-debug/lib/debug
I think I can reproduce in a local test case. The snag is that when the transfer gets paused and the HTTP/2 stream window size gets exhausted, the unpausing does not send a window update.
By default, curl's stream window size is 10 MB, which is in your example the data received by the stalled stream.
When pausing a HTTP/2 transfer, the stream's local window size
is reduced to 0 to prevent the server from sending further data
which curl cannot write out to the application.
When unpausing again, the stream's window size was not correctly
increased again. The attempt to trigger a window update was
ignored by nghttp2, the server never received it and the transfer
stalled.
Add a debug feature to allow use of small window sizes which
reproduces this bug in test_02_21.
Fixescurl#16955Closescurl#16960
I did this
You can clone the whole repro from https://gist.github.com/lf-/276cb01858d894f4946787f69254a923.
Upstream bug report: https://git.lix.systems/lix-project/lix/issues/662
This is the bug I promised to report after #16280 was fixed.
What is going wrong?
Curl fails to send window size updates when large differences exist in HTTP/2 stream throughput when using pauses.
This then presents as the transfer getting deadlocked, since Curl fails to request more data with more window size.
Symptoms
The first transfer finishes, the second transfer has a bunch of bytes buffered up after its initial window size, and the receiver is slowly drip-feeding them out.
First one done!
At this point it's stuck and will not make any forward progress:
The streams have received the following approximate amounts of data at time of deadlock:
I have captured a packet capture of the problem with
SSLKEYLOGFILE
and tcpdump and observed that there are numerous WINDOW_UPDATE frames for stream 1 and only one (at the very beginning) for stream 3 (the slow one).That is to say, stream 3 has more data to read from the server, but curl didn't ask the server for it and is sitting twiddling its thumbs hoping someone, anyone, will give it some data.
Reproducer
This is a reproducer using a hacked up version of the Lix HTTP library.
There are two streams being fetched via the same connection, the latter of which is much much slower to read.
They are both fetching the same 100MB file of urandom garbage.
The data being fetched is zstd encoded (unsure if this matters; curl compression support is disabled for testing purposes).
Caddyfile:
Short version of the reproducer:
For the runnable version, see
repro.cc
and Makefile; it has been checked to build with Clang 18 on NixOS.I am deeply sorry about submitting 600 lines of C++ of repro.
I cut down the lines by about 50% over the original code, and it is now self-contained.
More reduction would take rewriting it and that would probably make the code harder to deal with for debugging.
If you need more reduction, I can do it, but it will take a while.
Set up the Caddy (2.8.4 was used) server as above (mkdir trash, dd some randomness), then run it:
Run the repro:
If you don't want to copy paste this stuff, you can
git clone https://gist.github.com/lf-/276cb01858d894f4946787f69254a923 repro/
Makefile
repro.cc
I expected the following
The second transfer of
bar
should not get stuck partially completed with no window size remaining.curl/libcurl version
COMMIT ID: 8a45c28, which was HEAD as of the time of writing
Here is the way to obtain an approximately byte-identical Curl that I used using Nix, if necessary: put this in a file, then
nix-build file.nix -A all
. The results will be inresult*
symlinks. If you have to debug a Nix build of Curl, useNIX_DEBUG_INFO_DIRS=result-debug/lib/debug
operating system
Linux nucury 6.12.19 #1-NixOS SMP PREEMPT_DYNAMIC Thu Mar 13 12:02:20 UTC 2025 x86_64 GNU/Linux
The text was updated successfully, but these errors were encountered: