From c828bbc586180cdd12a5d34fbb803dbc8de0e8de Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Tue, 1 Mar 2016 17:19:25 +0000 Subject: [PATCH] mod_http2: some DoS protection, fix for read after free git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1733113 13f79535-47bb-0310-9956-ffa450edef68 --- CHANGES | 12 ++ modules/http2/h2_io.c | 32 +++++- modules/http2/h2_io.h | 12 +- modules/http2/h2_mplx.c | 218 +++++++++++++++++++++++++++++++++---- modules/http2/h2_mplx.h | 21 +++- modules/http2/h2_push.c | 2 +- modules/http2/h2_request.c | 15 ++- modules/http2/h2_request.h | 4 +- modules/http2/h2_session.c | 10 +- modules/http2/h2_stream.c | 5 - modules/http2/h2_version.h | 4 +- modules/http2/h2_workers.c | 1 - 12 files changed, 294 insertions(+), 42 deletions(-) diff --git a/CHANGES b/CHANGES index 496dc73b87..8abcea4ed1 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,18 @@ -*- coding: utf-8 -*- Changes with Apache 2.5.0 + *) mod_http2: Fixed possible read after free when streams were cancelled early + by the client. + Fixed apr_uint64_t formatting in a log statement to user proper APR def. + Number of worker threads allowed to a connection is adjusting dynamically. + Starting with 4, the number is doubled when streams can be served without + the server ever having to wait on the client. The number is halfed, when + the server has to wait on flow control grants. This can happen with a + maximum frequency of 5 times per second. When a connection occupies too + many workers, repeatable requests (GET/HEAD/OPTIONS) are cancelled and + placed back in the queue. Should that not suffice and a stream is busy + longer than the server timeout, the connection will be aborted. + *) mod_ssl: Fix a possible memory leak on restart for custom [EC]DH params. [Jan Kaluza, Yann Ylavic] diff --git a/modules/http2/h2_io.c b/modules/http2/h2_io.c index f88ac4d880..a54e8763b7 100644 --- a/modules/http2/h2_io.c +++ b/modules/http2/h2_io.c @@ -33,17 +33,47 @@ #include "h2_task.h" #include "h2_util.h" -h2_io *h2_io_create(int id, apr_pool_t *pool) +h2_io *h2_io_create(int id, apr_pool_t *pool, const h2_request *request) { h2_io *io = apr_pcalloc(pool, sizeof(*io)); if (io) { io->id = id; io->pool = pool; io->bucket_alloc = apr_bucket_alloc_create(pool); + io->request = h2_request_clone(pool, request); } return io; } +void h2_io_redo(h2_io *io) +{ + io->worker_started = 0; + io->response = NULL; + io->rst_error = 0; + if (io->bbin) { + apr_brigade_cleanup(io->bbin); + } + if (io->bbout) { + apr_brigade_cleanup(io->bbout); + } + if (io->tmp) { + apr_brigade_cleanup(io->tmp); + } + io->started_at = io->done_at = 0; +} + +int h2_io_is_repeatable(h2_io *io) { + if (io->submitted + || io->input_consumed > 0 + || !io->request) { + /* cannot repeat that. */ + return 0; + } + return (!strcmp("GET", io->request->method) + || !strcmp("HEAD", io->request->method) + || !strcmp("OPTIONS", io->request->method)); +} + void h2_io_set_response(h2_io *io, h2_response *response) { AP_DEBUG_ASSERT(io->pool); diff --git a/modules/http2/h2_io.h b/modules/http2/h2_io.h index 7c704a4d8e..bfe42a96b4 100644 --- a/modules/http2/h2_io.h +++ b/modules/http2/h2_io.h @@ -49,8 +49,9 @@ struct h2_io { apr_bucket_brigade *tmp; /* temporary data for chunking */ unsigned int orphaned : 1; /* h2_stream is gone for this io */ - unsigned int processing_started : 1; /* h2_worker started processing for this io */ - unsigned int processing_done: 1; /* h2_worker finished for this io */ + unsigned int worker_started : 1; /* h2_worker started processing for this io */ + unsigned int worker_done : 1; /* h2_worker finished for this io */ + unsigned int submitted : 1; /* response has been submitted to client */ unsigned int request_body : 1; /* iff request has body */ unsigned int eos_in : 1; /* input eos has been seen */ unsigned int eos_in_written : 1; /* input eos has been forwarded */ @@ -61,6 +62,8 @@ struct h2_io { struct apr_thread_cond_t *timed_cond; /* condition to wait on, maybe NULL */ apr_time_t timeout_at; /* when IO wait will time out */ + apr_time_t started_at; /* when processing started */ + apr_time_t done_at; /* when processing was done */ apr_size_t input_consumed; /* how many bytes have been read */ int files_handles_owned; @@ -73,7 +76,7 @@ struct h2_io { /** * Creates a new h2_io for the given stream id. */ -h2_io *h2_io_create(int id, apr_pool_t *pool); +h2_io *h2_io_create(int id, apr_pool_t *pool, const struct h2_request *request); /** * Set the response of this stream. @@ -85,6 +88,9 @@ void h2_io_set_response(h2_io *io, struct h2_response *response); */ void h2_io_rst(h2_io *io, int error); +int h2_io_is_repeatable(h2_io *io); +void h2_io_redo(h2_io *io); + /** * The input data is completely queued. Blocked reads will return immediately * and give either data or EOF. diff --git a/modules/http2/h2_mplx.c b/modules/http2/h2_mplx.c index 6311d67d4e..b40a137ebd 100644 --- a/modules/http2/h2_mplx.c +++ b/modules/http2/h2_mplx.c @@ -209,7 +209,11 @@ h2_mplx *h2_mplx_create(conn_rec *c, apr_pool_t *parent, m->stream_max_mem = h2_config_geti(conf, H2_CONF_STREAM_MAX_MEM); m->stream_timeout = stream_timeout; m->workers = workers; - m->workers_max = 6; + m->workers_max = h2_config_geti(conf, H2_CONF_MAX_WORKERS); + m->workers_def_limit = 4; + m->workers_limit = m->workers_def_limit; + m->last_limit_change = m->last_idle_block = apr_time_now(); + m->limit_change_interval = apr_time_from_msec(200); m->tx_handles_reserved = 0; m->tx_chunk_size = 4; @@ -276,6 +280,9 @@ static void io_destroy(h2_mplx *m, h2_io *io, int events) h2_io_set_remove(m->stream_ios, io); h2_io_set_remove(m->ready_ios, io); + if (m->redo_ios) { + h2_io_set_remove(m->redo_ios, io); + } if (pool) { apr_pool_clear(pool); @@ -292,7 +299,7 @@ static int io_stream_done(h2_mplx *m, h2_io *io, int rst_error) { /* Remove io from ready set, we will never submit it */ h2_io_set_remove(m->ready_ios, io); - if (!io->processing_started || io->processing_done) { + if (!io->worker_started || io->worker_done) { /* already finished or not even started yet */ h2_iq_remove(m->q, io->id); io_destroy(m, io, 1); @@ -321,7 +328,7 @@ static int stream_print(void *ctx, h2_io *io) io->request->method, io->request->authority, io->request->path, io->response? "http" : (io->rst_error? "reset" : "?"), io->response? io->response->http_status : io->rst_error, - io->orphaned, io->processing_started, io->processing_done, + io->orphaned, io->worker_started, io->worker_done, io->eos_in, io->eos_out); } else if (io) { @@ -331,7 +338,7 @@ static int stream_print(void *ctx, h2_io *io) m->id, io->id, io->response? "http" : (io->rst_error? "reset" : "?"), io->response? io->response->http_status : io->rst_error, - io->orphaned, io->processing_started, io->processing_done, + io->orphaned, io->worker_started, io->worker_done, io->eos_in, io->eos_out); } else { @@ -647,6 +654,7 @@ h2_stream *h2_mplx_next_submit(h2_mplx *m, h2_ihash_t *streams) if (io && !m->aborted) { stream = h2_ihash_get(streams, io->id); if (stream) { + io->submitted = 1; if (io->rst_error) { h2_stream_rst(stream, io->rst_error); } @@ -667,7 +675,7 @@ h2_stream *h2_mplx_next_submit(h2_mplx *m, h2_ihash_t *streams) "resetting io to close request processing", m->id, io->id); h2_io_make_orphaned(io, H2_ERR_STREAM_CLOSED); - if (!io->processing_started || io->processing_done) { + if (!io->worker_started || io->worker_done) { io_destroy(m, io, 1); } else { @@ -989,7 +997,7 @@ apr_status_t h2_mplx_reprioritize(h2_mplx *m, h2_stream_pri_cmp *cmp, void *ctx) return status; } -static h2_io *open_io(h2_mplx *m, int stream_id) +static h2_io *open_io(h2_mplx *m, int stream_id, const h2_request *request) { apr_pool_t *io_pool = m->spare_pool; h2_io *io; @@ -1002,7 +1010,7 @@ static h2_io *open_io(h2_mplx *m, int stream_id) m->spare_pool = NULL; } - io = h2_io_create(stream_id, io_pool); + io = h2_io_create(stream_id, io_pool, request); h2_io_set_add(m->stream_ios, io); return io; @@ -1022,8 +1030,7 @@ apr_status_t h2_mplx_process(h2_mplx *m, int stream_id, const h2_request *req, status = APR_ECONNABORTED; } else { - h2_io *io = open_io(m, stream_id); - io->request = req; + h2_io *io = open_io(m, stream_id, req); if (!io->request->body) { status = h2_io_in_close(io); @@ -1050,15 +1057,21 @@ static h2_task *pop_task(h2_mplx *m) h2_task *task = NULL; int sid; while (!m->aborted && !task - && (m->workers_busy < m->workers_max) + && (m->workers_busy < m->workers_limit) && (sid = h2_iq_shift(m->q)) > 0) { h2_io *io = h2_io_set_get(m->stream_ios, sid); - if (io) { + if (io && io->orphaned) { + io_destroy(m, io, 0); + if (m->join_wait) { + apr_thread_cond_signal(m->join_wait); + } + } + else if (io) { conn_rec *slave = h2_slave_create(m->c, m->pool, m->spare_allocator); m->spare_allocator = NULL; task = h2_task_create(m->id, io->request, slave, m); - - io->processing_started = 1; + io->worker_started = 1; + io->started_at = apr_time_now(); if (sid > m->max_stream_started) { m->max_stream_started = sid; } @@ -1102,6 +1115,7 @@ static void task_done(h2_mplx *m, h2_task *task) } else { h2_io *io = h2_io_set_get(m->stream_ios, task->stream_id); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, "h2_mplx(%ld): task(%s) done", m->id, task->id); /* clean our references and report request as done. Signal @@ -1117,7 +1131,39 @@ static void task_done(h2_mplx *m, h2_task *task) h2_slave_destroy(task->c, &m->spare_allocator); task = NULL; if (io) { - io->processing_done = 1; + apr_time_t now = apr_time_now(); + if (!io->orphaned && m->redo_ios + && h2_io_set_get(m->redo_ios, io->id)) { + /* reset and schedule again */ + h2_io_redo(io); + h2_io_set_remove(m->redo_ios, io); + h2_iq_add(m->q, io->id, NULL, NULL); + } + else { + io->worker_done = 1; + io->done_at = now; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, + "h2_mplx(%ld): request(%d) done, %f ms" + " elapsed", m->id, io->id, + (io->done_at - io->started_at) / 1000.0); + if (io->started_at > m->last_idle_block) { + /* this task finished without causing an 'idle block', e.g. + * a block by flow control. + */ + if (now - m->last_limit_change >= m->limit_change_interval + && m->workers_limit < m->workers_max) { + /* Well behaving stream, allow it more workers */ + m->workers_limit = H2MIN(m->workers_limit * 2, + m->workers_max); + m->last_limit_change = now; + m->need_registration = 1; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, + "h2_mplx(%ld): increase worker limit to %d", + m->id, m->workers_limit); + } + } + } + if (io->orphaned) { io_destroy(m, io, 0); if (m->join_wait) { @@ -1131,12 +1177,11 @@ static void task_done(h2_mplx *m, h2_task *task) apr_thread_cond_broadcast(m->task_done); } } - } void h2_mplx_task_done(h2_mplx *m, h2_task *task, h2_task **ptask) { - int acquired, do_registration = 0; + int acquired; if (enter_mutex(m, &acquired) == APR_SUCCESS) { task_done(m, task); @@ -1145,12 +1190,147 @@ void h2_mplx_task_done(h2_mplx *m, h2_task *task, h2_task **ptask) /* caller wants another task */ *ptask = pop_task(m); } - do_registration = (m->workers_busy+1 == m->workers_max); leave_mutex(m, acquired); } - if (do_registration) { - workers_register(m); +} + +/******************************************************************************* + * h2_mplx DoS protection + ******************************************************************************/ + +typedef struct { + h2_mplx *m; + h2_io *io; + apr_time_t now; +} io_iter_ctx; + +static int latest_repeatable_busy_unsubmitted_iter(void *data, h2_io *io) +{ + io_iter_ctx *ctx = data; + if (io->worker_started && !io->worker_done + && h2_io_is_repeatable(io) + && !h2_io_set_get(ctx->m->redo_ios, io->id)) { + /* this io occupies a worker, the response has not been submitted yet, + * not been cancelled and it is a repeatable request + * -> it can be re-scheduled later */ + if (!ctx->io || ctx->io->started_at < io->started_at) { + /* we did not have one or this one was started later */ + ctx->io = io; + } + } + return 1; +} + +static h2_io *get_latest_repeatable_busy_unsubmitted_io(h2_mplx *m) +{ + io_iter_ctx ctx; + ctx.m = m; + ctx.io = NULL; + h2_io_set_iter(m->stream_ios, latest_repeatable_busy_unsubmitted_iter, &ctx); + return ctx.io; +} + +static int timed_out_busy_iter(void *data, h2_io *io) +{ + io_iter_ctx *ctx = data; + if (io->worker_started && !io->worker_done + && (ctx->now - io->started_at) > ctx->m->stream_timeout) { + /* timed out stream occupying a worker, found */ + ctx->io = io; + return 0; } + return 1; +} +static h2_io *get_timed_out_busy_stream(h2_mplx *m) +{ + io_iter_ctx ctx; + ctx.m = m; + ctx.io = NULL; + ctx.now = apr_time_now(); + h2_io_set_iter(m->stream_ios, timed_out_busy_iter, &ctx); + return ctx.io; +} + +static apr_status_t unschedule_slow_ios(h2_mplx *m) +{ + h2_io *io; + int n; + + if (!m->redo_ios) { + m->redo_ios = h2_io_set_create(m->pool); + } + /* Try to get rid of streams that occupy workers. Look for safe requests + * that are repeatable. If none found, fail the connection. + */ + n = (m->workers_busy - m->workers_limit - h2_io_set_size(m->redo_ios)); + while (n > 0 && (io = get_latest_repeatable_busy_unsubmitted_io(m))) { + h2_io_set_add(m->redo_ios, io); + h2_io_rst(io, H2_ERR_CANCEL); + --n; + } + + if ((m->workers_busy - h2_io_set_size(m->redo_ios)) > m->workers_limit) { + io = get_timed_out_busy_stream(m); + if (io) { + /* Too many busy workers, unable to cancel enough streams + * and with a busy, timed out stream, we tell the client + * to go away... */ + return APR_TIMEUP; + } + } + return APR_SUCCESS; +} + +apr_status_t h2_mplx_idle(h2_mplx *m) +{ + apr_status_t status = APR_SUCCESS; + apr_time_t now; + int acquired; + + if (enter_mutex(m, &acquired) == APR_SUCCESS) { + apr_size_t scount = h2_io_set_size(m->stream_ios); + if (scount > 0 && m->workers_busy) { + /* If we have streams in connection state 'IDLE', meaning + * all streams are ready to sent data out, but lack + * WINDOW_UPDATEs. + * + * This is ok, unless we have streams that still occupy + * h2 workers. As worker threads are a scarce resource, + * we need to take measures that we do not get DoSed. + * + * This is what we call an 'idle block'. Limit the amount + * of busy workers we allow for this connection until it + * well behaves. + */ + now = apr_time_now(); + m->last_idle_block = now; + if (m->workers_limit > 2 + && now - m->last_limit_change >= m->limit_change_interval) { + if (m->workers_limit > 16) { + m->workers_limit = 16; + } + else if (m->workers_limit > 8) { + m->workers_limit = 8; + } + else if (m->workers_limit > 4) { + m->workers_limit = 4; + } + else if (m->workers_limit > 2) { + m->workers_limit = 2; + } + m->last_limit_change = now; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, + "h2_mplx(%ld): decrease worker limit to %d", + m->id, m->workers_limit); + } + + if (m->workers_busy > m->workers_limit) { + status = unschedule_slow_ios(m); + } + } + leave_mutex(m, acquired); + } + return status; } /******************************************************************************* diff --git a/modules/http2/h2_mplx.h b/modules/http2/h2_mplx.h index 8dff6e0853..4d6ce7c0d5 100644 --- a/modules/http2/h2_mplx.h +++ b/modules/http2/h2_mplx.h @@ -68,15 +68,22 @@ struct h2_mplx { apr_pool_t *pool; unsigned int aborted : 1; + unsigned int need_registration : 1; struct h2_int_queue *q; struct h2_io_set *stream_ios; struct h2_io_set *ready_ios; + struct h2_io_set *redo_ios; int max_stream_started; /* highest stream id that started processing */ int workers_busy; /* # of workers processing on this mplx */ - int workers_max; /* max # of workers occupied by this mplx */ - int need_registration; + int workers_limit; /* current # of workers limit, dynamic */ + int workers_def_limit; /* default # of workers limit */ + int workers_max; /* max, hard limit # of workers in a process */ + apr_time_t last_idle_block; /* last time, this mplx entered IDLE while + * streams were ready */ + apr_time_t last_limit_change;/* last time, worker limit changed */ + apr_interval_time_t limit_change_interval; apr_thread_mutex_t *lock; struct apr_thread_cond_t *added_output; @@ -389,6 +396,16 @@ APR_RING_INSERT_TAIL((b), ap__b, h2_mplx, link); \ */ #define H2_MPLX_REMOVE(e) APR_RING_REMOVE((e), link) +/******************************************************************************* + * h2_mplx DoS protection + ******************************************************************************/ + +/** + * Master connection has entered idle mode. + * @param m the mplx instance of the master connection + * @return != SUCCESS iff connection should be terminated + */ +apr_status_t h2_mplx_idle(h2_mplx *m); /******************************************************************************* * h2_mplx h2_req_engine handling. diff --git a/modules/http2/h2_push.c b/modules/http2/h2_push.c index c0325ff419..748e32abbf 100644 --- a/modules/http2/h2_push.c +++ b/modules/http2/h2_push.c @@ -778,7 +778,7 @@ static apr_status_t gset_encode_next(gset_encoder *encoder, apr_uint64_t pval) /* Intentional no APLOGNO */ ap_log_perror(APLOG_MARK, GCSLOG_LEVEL, 0, encoder->pool, "h2_push_diary_enc: val=%"APR_UINT64_T_HEX_FMT", delta=%" - APR_UINT64_T_HEX_FMT" flex_bits=%" APR_UINT64_T_FMT + APR_UINT64_T_HEX_FMT" flex_bits=%"APR_UINT64_T_FMT", " ", fixed_bits=%d, fixed_val=%"APR_UINT64_T_HEX_FMT, pval, delta, flex_bits, encoder->fixed_bits, delta&encoder->fixed_mask); for (; flex_bits != 0; --flex_bits) { diff --git a/modules/http2/h2_request.c b/modules/http2/h2_request.c index 9aa8d49e5e..2767ef538a 100644 --- a/modules/http2/h2_request.c +++ b/modules/http2/h2_request.c @@ -60,10 +60,6 @@ h2_request *h2_request_createn(int id, apr_pool_t *pool, return req; } -void h2_request_destroy(h2_request *req) -{ -} - static apr_status_t inspect_clen(h2_request *req, const char *s) { char *end; @@ -342,11 +338,22 @@ void h2_request_copy(apr_pool_t *p, h2_request *dst, const h2_request *src) dst->authority = OPT_COPY(p, src->authority); dst->path = OPT_COPY(p, src->path); dst->headers = apr_table_clone(p, src->headers); + if (src->trailers) { + dst->trailers = apr_table_clone(p, src->trailers); + } dst->content_length = src->content_length; dst->chunked = src->chunked; dst->eoh = src->eoh; } +h2_request *h2_request_clone(apr_pool_t *p, const h2_request *src) +{ + h2_request *nreq = apr_pcalloc(p, sizeof(*nreq)); + memcpy(nreq, src, sizeof(*nreq)); + h2_request_copy(p, nreq, src); + return nreq; +} + request_rec *h2_request_create_rec(const h2_request *req, conn_rec *conn) { request_rec *r; diff --git a/modules/http2/h2_request.h b/modules/http2/h2_request.h index 946bd34852..da87d70a50 100644 --- a/modules/http2/h2_request.h +++ b/modules/http2/h2_request.h @@ -30,8 +30,6 @@ apr_status_t h2_request_make(h2_request *req, apr_pool_t *pool, const char *authority, const char *path, apr_table_t *headers); -void h2_request_destroy(h2_request *req); - apr_status_t h2_request_rwrite(h2_request *req, request_rec *r); apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool, @@ -47,6 +45,8 @@ apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, void h2_request_copy(apr_pool_t *p, h2_request *dst, const h2_request *src); +h2_request *h2_request_clone(apr_pool_t *p, const h2_request *src); + /** * Create a request_rec representing the h2_request to be * processed on the given connection. diff --git a/modules/http2/h2_session.c b/modules/http2/h2_session.c index 4019bd4d35..33f82fad9c 100644 --- a/modules/http2/h2_session.c +++ b/modules/http2/h2_session.c @@ -2015,7 +2015,7 @@ apr_status_t h2_session_process(h2_session *session, int async) no_streams = h2_ihash_is_empty(session->streams); update_child_status(session, (no_streams? SERVER_BUSY_KEEPALIVE : SERVER_BUSY_READ), "idle"); - if (async && !session->r && session->requests_received && no_streams) { + if (async && no_streams && !session->r && session->requests_received) { ap_log_cerror( APLOG_MARK, APLOG_TRACE1, status, c, "h2_session(%ld): async idle, nonblock read", session->id); /* We do not return to the async mpm immediately, since under @@ -2051,7 +2051,13 @@ apr_status_t h2_session_process(h2_session *session, int async) } else { /* We wait in smaller increments, using a 1 second timeout. - * That gives us the chance to check for MPMQ_STOPPING often. */ + * That gives us the chance to check for MPMQ_STOPPING often. + */ + status = h2_mplx_idle(session->mplx); + if (status != APR_SUCCESS) { + dispatch_event(session, H2_SESSION_EV_CONN_ERROR, + H2_ERR_ENHANCE_YOUR_CALM, "less is more"); + } h2_filter_cin_timeout_set(session->cin, apr_time_from_sec(1)); status = h2_session_read(session, 1); if (status == APR_SUCCESS) { diff --git a/modules/http2/h2_stream.c b/modules/http2/h2_stream.c index 8af65673a4..29df7afd82 100644 --- a/modules/http2/h2_stream.c +++ b/modules/http2/h2_stream.c @@ -169,11 +169,6 @@ h2_stream *h2_stream_open(int id, apr_pool_t *pool, h2_session *session) apr_status_t h2_stream_destroy(h2_stream *stream) { AP_DEBUG_ASSERT(stream); - if (stream->request) { - h2_request_destroy(stream->request); - stream->request = NULL; - } - if (stream->pool) { apr_pool_destroy(stream->pool); } diff --git a/modules/http2/h2_version.h b/modules/http2/h2_version.h index f02560cedb..b828cf784d 100644 --- a/modules/http2/h2_version.h +++ b/modules/http2/h2_version.h @@ -26,7 +26,7 @@ * @macro * Version number of the http2 module as c string */ -#define MOD_HTTP2_VERSION "1.3.1-DEV" +#define MOD_HTTP2_VERSION "1.3.2-DEV" /** * @macro @@ -34,7 +34,7 @@ * release. This is a 24 bit number with 8 bits for major number, 8 bits * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. */ -#define MOD_HTTP2_VERSION_NUM 0x010301 +#define MOD_HTTP2_VERSION_NUM 0x010302 #endif /* mod_h2_h2_version_h */ diff --git a/modules/http2/h2_workers.c b/modules/http2/h2_workers.c index 6b6897cbe5..2c1dc8dab4 100644 --- a/modules/http2/h2_workers.c +++ b/modules/http2/h2_workers.c @@ -86,7 +86,6 @@ static h2_task *next_task(h2_workers *workers) --workers->mplx_count; task = h2_mplx_pop_task(m, &has_more); - if (has_more) { H2_MPLX_LIST_INSERT_TAIL(&workers->mplxs, m); ++workers->mplx_count; -- 2.50.0