Skip to content

Commit 16c4611

Browse files
committed
More strict stream state handling
Previously, in server side, we used closed streams to detect the error that the misbehaving client sends a frame on the incoming stream it explicitly closed. With this commit, we make a further step, and detect one more error case. Since we retain closed streams as long as the sum of its size and the number of opened streams are equal or less than max concurrent streams, we can safely say that if we get a frame which is sent on the stream that is not found in either closed or opened stream, it is already closed or has not existed. Then we can send GOAWAY. The previous code shrinks closed streams when we closed another stream, but now it is removed. It is enough to adjust closed streams when new incoming stream is created. While creating this commit, we noticed that NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS is defined as INT32_MAX. But since SETTINGS can contain value up to UINT32_MAX, it is not enough. However, since the stream ID space is limited to INT32_MAX, it is high enough. We could keep this value, but this time we deprecate NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS macro. While it is in public header, the effect of deprecating it is negligible because of the reason we wrote above, and usually application sets much smaller value (say, 100) as SETTINGS_MAX_CONCURRENT_STREAMS.
1 parent 862175b commit 16c4611

6 files changed

Lines changed: 282 additions & 20 deletions

File tree

lib/includes/nghttp2/nghttp2.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,10 @@ typedef enum {
668668
/**
669669
* @macro
670670
*
671+
* .. warning::
672+
*
673+
* Deprecated. The initial max concurrent streams is 0xffffffffu.
674+
*
671675
* Default maximum number of incoming concurrent streams. Use
672676
* `nghttp2_submit_settings()` with
673677
* :enum:`NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS` to change the

lib/nghttp2_session.c

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ static void session_inbound_frame_reset(nghttp2_session *session) {
365365
static void init_settings(nghttp2_settings_storage *settings) {
366366
settings->header_table_size = NGHTTP2_HD_DEFAULT_MAX_BUFFER_SIZE;
367367
settings->enable_push = 1;
368-
settings->max_concurrent_streams = NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS;
368+
settings->max_concurrent_streams = NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS;
369369
settings->initial_window_size = NGHTTP2_INITIAL_WINDOW_SIZE;
370370
settings->max_frame_size = NGHTTP2_MAX_FRAME_SIZE_MIN;
371371
settings->max_header_list_size = UINT32_MAX;
@@ -435,7 +435,7 @@ static int session_new(nghttp2_session **session_ptr,
435435
(*session_ptr)->remote_last_stream_id = (1u << 31) - 1;
436436

437437
(*session_ptr)->pending_local_max_concurrent_stream =
438-
NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS;
438+
NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS;
439439
(*session_ptr)->pending_enable_push = 1;
440440

441441
if (server) {
@@ -1185,13 +1185,11 @@ int nghttp2_session_close_stream(nghttp2_session *session, int32_t stream_id,
11851185
combined with the current active incoming streams to make
11861186
dependency tree work better. */
11871187
nghttp2_session_keep_closed_stream(session, stream);
1188-
1189-
rv = nghttp2_session_adjust_closed_stream(session);
11901188
} else {
11911189
rv = nghttp2_session_destroy_stream(session, stream);
1192-
}
1193-
if (rv != 0) {
1194-
return rv;
1190+
if (rv != 0) {
1191+
return rv;
1192+
}
11951193
}
11961194

11971195
return 0;
@@ -1285,8 +1283,12 @@ int nghttp2_session_adjust_closed_stream(nghttp2_session *session) {
12851283
size_t num_stream_max;
12861284
int rv;
12871285

1288-
num_stream_max = nghttp2_min(session->local_settings.max_concurrent_streams,
1289-
session->pending_local_max_concurrent_stream);
1286+
if (session->local_settings.max_concurrent_streams ==
1287+
NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS) {
1288+
num_stream_max = session->pending_local_max_concurrent_stream;
1289+
} else {
1290+
num_stream_max = session->local_settings.max_concurrent_streams;
1291+
}
12901292

12911293
DEBUGF(fprintf(stderr, "stream: adjusting kept closed streams "
12921294
"num_closed_streams=%zu, num_incoming_streams=%zu, "
@@ -3777,22 +3779,74 @@ int nghttp2_session_on_request_headers_received(nghttp2_session *session,
37773779
assert(session->server);
37783780

37793781
if (!session_is_new_peer_stream_id(session, frame->hd.stream_id)) {
3780-
/* The spec says if an endpoint receives a HEADERS with invalid
3781-
stream ID, it MUST issue connection error with error code
3782-
PROTOCOL_ERROR. But we could get trailer HEADERS after we have
3783-
sent RST_STREAM to this stream and peer have not received it.
3784-
Then connection error is too harsh. It means that we only use
3785-
connection error if stream ID refers idle stream. Therwise we
3786-
just ignore HEADERS for now. */
37873782
if (frame->hd.stream_id == 0 ||
37883783
nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) {
37893784
return session_inflate_handle_invalid_connection(
37903785
session, frame, NGHTTP2_ERR_PROTO,
37913786
"request HEADERS: invalid stream_id");
37923787
}
37933788

3789+
/* RFC 7540 says if an endpoint receives a HEADERS with invalid
3790+
* stream ID (e.g, numerically smaller than previous), it MUST
3791+
* issue connection error with error code PROTOCOL_ERROR. It is a
3792+
* bit hard to detect this, since we cannot remember all streams
3793+
* we observed so far.
3794+
*
3795+
* You might imagine this is really easy. But no. HTTP/2 is
3796+
* asynchronous protocol, and usually client and server do not
3797+
* share the complete picture of open/closed stream status. For
3798+
* example, after server sends RST_STREAM for a stream, client may
3799+
* send trailer HEADERS for that stream. If naive server detects
3800+
* that, and issued connection error, then it is a bug of server
3801+
* implementation since client is not wrong if it did not get
3802+
* RST_STREAM when it issued trailer HEADERS.
3803+
*
3804+
* For server session, we remember closed streams as long as the
3805+
* sum of closed streams and opened streams are under current max
3806+
* concurrent streams. We can use these closed streams to detect
3807+
* the error in some cases.
3808+
*
3809+
* If the stream cannot be found in either closed or opened
3810+
* streams, it is considered to be closed, or it has not exist
3811+
* (e.g., peer skipped sending the stream). Actually, it is
3812+
* impossible to detect which is which, since that information was
3813+
* lost forever. For these cases, we send back GOAWAY with
3814+
* PROTOCOL_ERROR.
3815+
*
3816+
* If the stream is found, and we know that it is in half closed
3817+
* (remote), or closed by peer's explicit action (e.g., received
3818+
* RST_STREAM from peer, or peer sends HEADERS/DATA frame with
3819+
* END_STREAM), getting new frame on that stream is clearly error.
3820+
* In this case, we send GOAWAY with error code STREAM_CLOSED.
3821+
*
3822+
* There is one corner case here. Server can change the max
3823+
* concurrent streams. The initial value of max concurrent
3824+
* streams is unlimited (NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS,
3825+
* which is UINT32_MAX). When sending out SETTINGS with
3826+
* MAX_CONCURRENT_STREAMS, we save its value as pending max
3827+
* concurrent streams, and use it as a cap to remember closed
3828+
* stream to save memory. This means that we might not sure that
3829+
* stream surely closed or has not exist when it is not found in
3830+
* closed or opened stream. To workaround this issue, we ignore
3831+
* incoming frame if the current max concurrent streams is
3832+
* NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS, and pending max
3833+
* concurrent streams is less than that.
3834+
*/
37943835
stream = nghttp2_session_get_stream_raw(session, frame->hd.stream_id);
3795-
if (stream && (stream->shut_flags & NGHTTP2_SHUT_RD)) {
3836+
3837+
if (!stream) {
3838+
if (session->local_settings.max_concurrent_streams ==
3839+
NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS &&
3840+
session->pending_local_max_concurrent_stream <
3841+
NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS) {
3842+
return NGHTTP2_ERR_IGN_HEADER_BLOCK;
3843+
}
3844+
3845+
return session_inflate_handle_invalid_connection(
3846+
session, frame, NGHTTP2_ERR_PROTO, "HEADERS: stream does not exist");
3847+
}
3848+
3849+
if (stream->shut_flags & NGHTTP2_SHUT_RD) {
37963850
return session_inflate_handle_invalid_connection(
37973851
session, frame, NGHTTP2_ERR_STREAM_CLOSED, "HEADERS: stream closed");
37983852
}
@@ -5066,7 +5120,25 @@ static int session_on_data_received_fail_fast(nghttp2_session *session) {
50665120
stream = nghttp2_session_get_stream(session, stream_id);
50675121
if (!stream) {
50685122
stream = nghttp2_session_get_stream_raw(session, stream_id);
5069-
if (stream && (stream->shut_flags & NGHTTP2_SHUT_RD)) {
5123+
5124+
if (!stream) {
5125+
if (session->server) {
5126+
if (session->local_settings.max_concurrent_streams ==
5127+
NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS &&
5128+
session->pending_local_max_concurrent_stream <
5129+
NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS) {
5130+
return NGHTTP2_ERR_IGN_PAYLOAD;
5131+
}
5132+
5133+
failure_reason = "DATA: stream does not exist";
5134+
error_code = NGHTTP2_PROTOCOL_ERROR;
5135+
goto fail;
5136+
}
5137+
5138+
return NGHTTP2_ERR_IGN_PAYLOAD;
5139+
}
5140+
5141+
if (stream->shut_flags & NGHTTP2_SHUT_RD) {
50705142
failure_reason = "DATA: stream closed";
50715143
error_code = NGHTTP2_STREAM_CLOSED;
50725144
goto fail;

lib/nghttp2_session.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ typedef struct {
9797
these frames in this number, it is considered suspicious. */
9898
#define NGHTTP2_MAX_OBQ_FLOOD_ITEM 10000
9999

100+
/* The default value of maximum number of concurrent streams. */
101+
#define NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS 0xffffffffu
102+
100103
/* Internal state when receiving incoming frame */
101104
typedef enum {
102105
/* Receiving frame header */

tests/main.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ int main(int argc _U_, char *argv[] _U_) {
308308
test_nghttp2_session_set_local_window_size) ||
309309
!CU_add_test(pSuite, "session_cancel_from_before_frame_send",
310310
test_nghttp2_session_cancel_from_before_frame_send) ||
311+
!CU_add_test(pSuite, "session_removed_closed_stream",
312+
test_nghttp2_session_removed_closed_stream) ||
311313
!CU_add_test(pSuite, "http_mandatory_headers",
312314
test_nghttp2_http_mandatory_headers) ||
313315
!CU_add_test(pSuite, "http_content_length",

tests/nghttp2_session_test.c

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2366,7 +2366,7 @@ void test_nghttp2_session_on_request_headers_received(void) {
23662366

23672367
nghttp2_frame_headers_free(&frame.headers, mem);
23682368
session->local_settings.max_concurrent_streams =
2369-
NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS;
2369+
NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS;
23702370

23712371
/* Stream ID less than or equal to the previouly received request
23722372
HEADERS is just ignored due to race condition */
@@ -5042,7 +5042,7 @@ void test_nghttp2_submit_settings(void) {
50425042
nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, iv, 7));
50435043

50445044
/* Make sure that local settings are not changed */
5045-
CU_ASSERT(NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS ==
5045+
CU_ASSERT(NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS ==
50465046
session->local_settings.max_concurrent_streams);
50475047
CU_ASSERT(NGHTTP2_INITIAL_WINDOW_SIZE ==
50485048
session->local_settings.initial_window_size);
@@ -9715,6 +9715,186 @@ void test_nghttp2_session_cancel_from_before_frame_send(void) {
97159715
nghttp2_session_del(session);
97169716
}
97179717

9718+
static void
9719+
prepare_session_removed_closed_stream(nghttp2_session *session,
9720+
nghttp2_hd_deflater *deflater) {
9721+
int rv;
9722+
nghttp2_settings_entry iv;
9723+
nghttp2_bufs bufs;
9724+
nghttp2_mem *mem;
9725+
ssize_t nread;
9726+
int i;
9727+
nghttp2_stream *stream;
9728+
nghttp2_frame_hd hd;
9729+
9730+
mem = nghttp2_mem_default();
9731+
9732+
frame_pack_bufs_init(&bufs);
9733+
9734+
iv.settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS;
9735+
iv.value = 2;
9736+
9737+
rv = nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, &iv, 1);
9738+
9739+
CU_ASSERT(0 == rv);
9740+
9741+
rv = nghttp2_session_send(session);
9742+
9743+
CU_ASSERT(0 == rv);
9744+
9745+
for (i = 1; i <= 3; i += 2) {
9746+
rv = pack_headers(&bufs, deflater, i,
9747+
NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM, reqnv,
9748+
ARRLEN(reqnv), mem);
9749+
9750+
CU_ASSERT(0 == rv);
9751+
9752+
nread = nghttp2_session_mem_recv(session, bufs.head->buf.pos,
9753+
nghttp2_bufs_len(&bufs));
9754+
9755+
CU_ASSERT((ssize_t)nghttp2_bufs_len(&bufs) == nread);
9756+
9757+
nghttp2_bufs_reset(&bufs);
9758+
}
9759+
9760+
nghttp2_session_close_stream(session, 3, NGHTTP2_NO_ERROR);
9761+
9762+
rv = pack_headers(&bufs, deflater, 5,
9763+
NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM, reqnv,
9764+
ARRLEN(reqnv), mem);
9765+
9766+
CU_ASSERT(0 == rv);
9767+
9768+
/* Receiving stream 5 will erase stream 3 from closed stream list */
9769+
nread = nghttp2_session_mem_recv(session, bufs.head->buf.pos,
9770+
nghttp2_bufs_len(&bufs));
9771+
9772+
CU_ASSERT((ssize_t)nghttp2_bufs_len(&bufs) == nread);
9773+
9774+
stream = nghttp2_session_get_stream_raw(session, 3);
9775+
9776+
CU_ASSERT(NULL == stream);
9777+
9778+
/* Since the current max concurrent streams is
9779+
NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS, receiving frame on stream
9780+
3 is ignored. */
9781+
nghttp2_bufs_reset(&bufs);
9782+
rv = pack_headers(&bufs, deflater, 3,
9783+
NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM,
9784+
trailernv, ARRLEN(trailernv), mem);
9785+
9786+
CU_ASSERT(0 == rv);
9787+
9788+
nread = nghttp2_session_mem_recv(session, bufs.head->buf.pos,
9789+
nghttp2_bufs_len(&bufs));
9790+
9791+
CU_ASSERT((ssize_t)nghttp2_bufs_len(&bufs) == nread);
9792+
CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session));
9793+
9794+
nghttp2_frame_hd_init(&hd, 0, NGHTTP2_DATA, NGHTTP2_FLAG_NONE, 3);
9795+
nghttp2_bufs_reset(&bufs);
9796+
nghttp2_frame_pack_frame_hd(bufs.head->buf.last, &hd);
9797+
bufs.head->buf.last += NGHTTP2_FRAME_HDLEN;
9798+
9799+
nread = nghttp2_session_mem_recv(session, bufs.head->buf.pos,
9800+
nghttp2_bufs_len(&bufs));
9801+
9802+
CU_ASSERT((ssize_t)nghttp2_bufs_len(&bufs) == nread);
9803+
CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session));
9804+
9805+
/* Now server receives SETTINGS ACK */
9806+
nghttp2_frame_hd_init(&hd, 0, NGHTTP2_SETTINGS, NGHTTP2_FLAG_ACK, 0);
9807+
nghttp2_bufs_reset(&bufs);
9808+
nghttp2_frame_pack_frame_hd(bufs.head->buf.last, &hd);
9809+
bufs.head->buf.last += NGHTTP2_FRAME_HDLEN;
9810+
9811+
nread = nghttp2_session_mem_recv(session, bufs.head->buf.pos,
9812+
nghttp2_bufs_len(&bufs));
9813+
9814+
CU_ASSERT((ssize_t)nghttp2_bufs_len(&bufs) == nread);
9815+
9816+
nghttp2_bufs_free(&bufs);
9817+
}
9818+
9819+
void test_nghttp2_session_removed_closed_stream(void) {
9820+
nghttp2_session *session;
9821+
nghttp2_session_callbacks callbacks;
9822+
int rv;
9823+
nghttp2_hd_deflater deflater;
9824+
nghttp2_bufs bufs;
9825+
nghttp2_mem *mem;
9826+
ssize_t nread;
9827+
nghttp2_frame_hd hd;
9828+
nghttp2_outbound_item *item;
9829+
9830+
mem = nghttp2_mem_default();
9831+
9832+
frame_pack_bufs_init(&bufs);
9833+
9834+
memset(&callbacks, 0, sizeof(callbacks));
9835+
9836+
callbacks.send_callback = null_send_callback;
9837+
9838+
nghttp2_session_server_new(&session, &callbacks, NULL);
9839+
9840+
/* Now local max concurrent streams is still unlimited, pending max
9841+
concurrent streams is now 2. */
9842+
9843+
nghttp2_hd_deflate_init(&deflater, mem);
9844+
9845+
prepare_session_removed_closed_stream(session, &deflater);
9846+
9847+
/* Now current max concurrent streams is 2, so receiving frame on
9848+
stream 3 is treated as connection error */
9849+
nghttp2_bufs_reset(&bufs);
9850+
rv = pack_headers(&bufs, &deflater, 3,
9851+
NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM,
9852+
trailernv, ARRLEN(trailernv), mem);
9853+
9854+
CU_ASSERT(0 == rv);
9855+
9856+
nread = nghttp2_session_mem_recv(session, bufs.head->buf.pos,
9857+
nghttp2_bufs_len(&bufs));
9858+
9859+
CU_ASSERT((ssize_t)nghttp2_bufs_len(&bufs) == nread);
9860+
9861+
item = nghttp2_session_get_next_ob_item(session);
9862+
9863+
CU_ASSERT(NULL != item);
9864+
CU_ASSERT(NGHTTP2_GOAWAY == item->frame.hd.type);
9865+
CU_ASSERT(NGHTTP2_PROTOCOL_ERROR == item->frame.goaway.error_code);
9866+
9867+
nghttp2_hd_deflate_free(&deflater);
9868+
nghttp2_session_del(session);
9869+
9870+
nghttp2_session_server_new(&session, &callbacks, NULL);
9871+
nghttp2_hd_deflate_init(&deflater, mem);
9872+
/* Same setup, and then receive DATA instead of HEADERS */
9873+
9874+
prepare_session_removed_closed_stream(session, &deflater);
9875+
9876+
nghttp2_frame_hd_init(&hd, 0, NGHTTP2_DATA, NGHTTP2_FLAG_NONE, 3);
9877+
nghttp2_bufs_reset(&bufs);
9878+
nghttp2_frame_pack_frame_hd(bufs.head->buf.last, &hd);
9879+
bufs.head->buf.last += NGHTTP2_FRAME_HDLEN;
9880+
9881+
nread = nghttp2_session_mem_recv(session, bufs.head->buf.pos,
9882+
nghttp2_bufs_len(&bufs));
9883+
9884+
CU_ASSERT((ssize_t)nghttp2_bufs_len(&bufs) == nread);
9885+
9886+
item = nghttp2_session_get_next_ob_item(session);
9887+
9888+
CU_ASSERT(NULL != item);
9889+
CU_ASSERT(NGHTTP2_GOAWAY == item->frame.hd.type);
9890+
CU_ASSERT(NGHTTP2_PROTOCOL_ERROR == item->frame.goaway.error_code);
9891+
9892+
nghttp2_hd_deflate_free(&deflater);
9893+
nghttp2_session_del(session);
9894+
9895+
nghttp2_bufs_free(&bufs);
9896+
}
9897+
97189898
static void check_nghttp2_http_recv_headers_fail(
97199899
nghttp2_session *session, nghttp2_hd_deflater *deflater, int32_t stream_id,
97209900
int stream_state, const nghttp2_nv *nva, size_t nvlen) {

0 commit comments

Comments
 (0)