https://bz.apache.org/bugzilla/show_bug.cgi?id=70131

            Bug ID: 70131
           Summary: mod_http2: HTTP/2 stream left open when a response
                    starts but never reaches EOS (e.g. a CGI that hits the
                    server Timeout mid-body)
           Product: Apache httpd-2
           Version: 2.4.68
          Hardware: All
                OS: All
            Status: NEW
          Severity: major
          Priority: P2
         Component: mod_http2
          Assignee: [email protected]
          Reporter: [email protected]
  Target Milestone: ---

Created attachment 40193
  --> https://bz.apache.org/bugzilla/attachment.cgi?id=40193&action=edit
The fix plus the two regression tests and CGI.

Over HTTP/2, when a handler emits a final response (status, headers, optionally
some body) and then stops before sending EOS, the stream is left open without
sending DATA, END_STREAM, or RST_STREAM, and a client without an idle/stall
timeout waits indefinitely. A common trigger is a CGI that stalls past the
server `Timeout` (logged as AH01220 / AH00574). The same handler over HTTP/1.1
does not hang: the connection is closed and the client gets an error.

## Steps to reproduce

1. A CGI that sends headers and a little body, flushes, then sleeps past
Timeout:

       #!/bin/sh
       printf 'Content-Type: application/octet-stream\r\n\r\n'
       dd if=/dev/zero bs=1024 count=16 2>/dev/null | tr '\000' 'X'
       sleep 30

2. A vhost with TLS + h2 + mod_cgi, `Timeout 1` (also occurs at the default),
   ScriptAlias-ing the CGI.

3. Request it over HTTP/2 with a client ceiling:

       curl -sk --http2 --max-time 8 -o /dev/null -w '%{time_total}\n'
https://127.0.0.1:PORT/slow

Actual (2.4.x): the request runs to `--max-time` (unbounded without one); no
RST_STREAM. During local testing one idle request reproduces ~80% of the time;
several
concurrent streams on one connection were ~100%.

Expected: the stream is reset, as on HTTP/1.1 and as mod_http2 already does for
an
incomplete response elsewhere. Current trunk (2.5.x) resets on this repro.

## Analysis

mod_http2 already resets an incomplete response: in `stream_data_cb`
(`h2_stream.c`)
a pull returning APR_EOF on a beam that never delivered an EOS calls
`h2_stream_rst(...)` (`test_003_72` covers the in-process case). On the CGI
path it
does not fire reliably: the handler runs on a secondary connection and feeds
the
primary via an `h2_bucket_beam`; `c2_process` (`h2_c2.c`) closes the beam on an
incomplete response, and `h2_beam_is_complete()` reports a closed beam as
complete
regardless of EOS, so `s_c2_done()` does not abort. That leaves only the
`stream_data_cb` APR_EOF path, and a closed beam yields APR_EOF only after its
buffer
is fully drained, so the reset needs c1 to re-enter `stream_data_cb` and pull
once more
in the drained state. That re-entry rides on a c1/c2 output wakeup whose timing
is not
guaranteed, so on threaded MPMs it can be missed and the stream parks.

2.4.x reproduces on the threaded MPMs (event, worker) but not on current trunk,
despite identical mod_http2 reset code, so the difference appears to be in the
MPM, not
mod_http2. Reading the trunk source (not confirmed against a live trace), its
event MPM
re-drives the connection on its own pending output (`ap_run_output_pending` /
`ap_filter_should_yield`, absent in 2.4.x) rather than parking it on
client-read once
output is flushed, which would let the h2 session re-run and observe the
terminal beam.
The attached change sidesteps this at the mod_http2 layer, independent of the
MPM.

## Proposed fix (attached patch)

When `c2_process` finishes with a final response started but no EOS seen, abort
the
output beam instead of closing it. An aborted beam reports
`h2_beam_is_complete()`
false, so `s_c2_done()` takes its existing incomplete-output abort branch, and
`h2_beam_receive()` returns APR_ECONNABORTED on the next pull unconditionally
(before
the buffer is drained); either path resets the stream regardless of wakeup
timing.
Three files: add `response_eos_seen` to `h2_conn_ctx.h`; set it and switch
close to
abort in `h2_c2.c`; and mark header-only final responses
(`AP_STATUS_IS_HEADER_ONLY`:
204/304, plus HEAD) complete in `h2_c2_filter.c` so they are not aborted
(see bug 69580). The WebSocket CONNECT path (`h2_ws.c`) also sets
`has_final_response` but needs no change; the WebSocket suite
(`test_800_websockets`)
passes with the fix.

### Relationship to bug 69580

Complementary case to bug 69580 (r1924267): because an aborted beam takes the
`stream_data_cb` APR_ECONNABORTED branch, which lacks that fix's header-only
carve-out, the change excludes header-only responses (204/304/HEAD) from the
abort,
so a mod_cache 304 over h2 behaves correctly.

## Verification

- 2.4.x unpatched hangs; patched, the stream is reset and the request is
bounded.
- bug 69580 repro (mod_cache 304 over h2) stays clean with the fix.
- The http2 test suite passes on the patched build, including two added tests
in
  `test_105_timeout.py` (`test_h2_105_20`: a silent CGI is reset;
`test_h2_105_21`:
  a mod_cache 304 is not reset), each failing if its change is reverted; the
  unchanged complete-response path is covered by the suite's existing tests.
Built
  and tested on the 2.4.x GitHub Actions matrix (Linux + Windows; http2 job on
event
  and worker): https://github.com/mmontalbo/httpd/pull/2

-- 
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to