Hi, This patch prevents Serf from accessing an already destroyed allocator when freeing HTTP/2 HPACK buckets in error scenarios. One possible reproduction script for the issue is:
(This is not 100% stable, but close to that. DEBUG_DOUBLE_FREE should be defined.) > serf_get -c4 -n4 --http2 https://www.cloudflare.com Error running context: (120153) HTTP2 flow control limits exceeded Aborted (core dumped) See serf_http2__stream_setup_next_request() around http2_stream.c:378. The HPACK bucket is created using the request->allocator, and is then added into the pump's output queue. Certain error conditions, such as the one above, can cause the request to be destroyed _before_ the pump with the bucket still alive. Attempting to destroy this bucket will then result in accessing an already destroyed request->allocator. While the HTTP/2 implementation can handle error situations with enqueued output that contains buckets from a request, it requires marking a request with SERF_WRITING_STARTED, to allow the special cleanup logic in outgoing_request.c:47 to kick in. This is how things currently work for HTTP/2 requests with bodies, where the body bucket originates from a serf_request_t. Luckily, in this particular case with HPACK buckets and header-only requests, it's possible to avoid using the request->allocator, and allocate the new bucket using the HTTP/2 stream's allocator. Thus, the lifetime of the new bucket will no longer depend on the underlying serf_request_t. Please see the attached patch with the fix described above. The log message is included in the beginning of the patch file. Regards, Evgeny Kotkov
HTTP/2: Avoid accessing an already destroyed allocator when freeing HPACK buckets in certain error scenarios. One possible reproduction script for the issue is: > serf_get -c4 -n4 --http2 https://www.cloudflare.com Error running context: (120153) HTTP2 flow control limits exceeded Aborted (core dumped) See serf_http2__stream_setup_next_request() around http2_stream.c:378. The HPACK bucket is created using the request->allocator, and is then added into the pump's output queue. Certain error conditions, such as the one above, can cause the request to be destroyed _before_ the pump with the bucket still alive. Attempting to destroy this bucket will then result in accessing an already destroyed request->allocator. While the HTTP/2 implementation can handle error situations with enqueued output that contains buckets from a request, it requires marking a request with SERF_WRITING_STARTED, to allow the special cleanup logic in outgoing_request.c:47 to kick in. This is how things currently work for HTTP/2 requests with bodies, where the body bucket originates from a serf_request_t. Luckily, in this particular case with HPACK buckets and header-only requests, it's possible to avoid using the request->allocator, and allocate the new bucket using the HTTP/2 stream's allocator. Thus, the lifetime of the new bucket will no longer depend on the underlying serf_request_t. * protocols/http2_stream.c (serf_http2__stream_setup_next_request): Allocate the new HPACK bucket using the HTTP/2 stream's allocator. Comment on why we don't set SERF_WRITING_STARTED for header-only requests. Patch by: Evgeny Kotkov <evgeny.kotkov{_AT_}visualsvn.com> Index: protocols/http2_stream.c =================================================================== --- protocols/http2_stream.c (revision 1787257) +++ protocols/http2_stream.c (working copy) @@ -375,7 +375,7 @@ serf_http2__stream_setup_next_request(serf_http2_s &hpack, hpack_tbl, request->req_bkt, request->conn->host_info.scheme, - request->allocator); + stream->alloc); if (status) return status; @@ -387,7 +387,7 @@ serf_http2__stream_setup_next_request(serf_http2_s serf_bucket_t *agg; unsigned char priority_data[5]; - agg = serf_bucket_aggregate_create(request->allocator); + agg = serf_bucket_aggregate_create(stream->alloc); priority_data[0] = (ds->streamid >> 24) & 0x7F; /* bit 7 of [0] is the exclusive flag */ @@ -399,7 +399,7 @@ serf_http2__stream_setup_next_request(serf_http2_s serf_bucket_aggregate_append( agg, serf_bucket_simple_copy_create((void *)priority_data, - 5, request->allocator)); + 5, stream->alloc)); serf_bucket_aggregate_append(agg, hpack); hpack = agg; @@ -423,6 +423,9 @@ serf_http2__stream_setup_next_request(serf_http2_s if (end_stream) { stream->status = H2S_HALFCLOSED_LOCAL; /* Headers sent; no body */ + /* Don't set SERF_WRITING_STARTED for the request. At this point, + the outgoing buckets no longer depend on it, because there's no + body and we copied the headers to our HPACK bucket. */ return APR_SUCCESS; }