Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crash in esp_http_server request parsing (IDFGH-748) #3182

Closed
jimparis opened this issue Mar 14, 2019 · 5 comments
Closed

Crash in esp_http_server request parsing (IDFGH-748) #3182

jimparis opened this issue Mar 14, 2019 · 5 comments

Comments

@jimparis
Copy link

Running esp-idf recent master (ff020c3)

I'm seeing a crash in esp_http_server that was a giant stack overflow. I needed CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y in order to track it down. The cause was an unbounded memcpy in in httpd_unrecv:

/* Truncate if external buf_len is greater than pending_data buffer size */
ra->sd->pending_len = MIN(sizeof(ra->sd->pending_data), buf_len);

/* Copy data into internal pending_data buffer */
size_t offset = sizeof(ra->sd->pending_data) - ra->sd->pending_len;
memcpy(ra->sd->pending_data + offset, buf, buf_len);

Here, buf_len was 4294967294 == (unsigned)-2. Which is probably a bug in itself, but either way, that memcpy should be of length ra->sd->pending_len, right?
Changing it results in this error, instead of a crash:

E (27513) httpd_parse: pause_parsing: data too large for un-recv = -2
W (27523) httpd_parse: parse_block: parsing failed
W (27523) httpd_txrx: httpd_resp_send_err: 500 Internal Server Error - Server has encountered an unexpected error

Full debug output from httpd_txrx.c and httpd_parse.c leading up to this error is as follows:

I (39824) httpd: httpd_server: processing socket 62
I (39834) httpd_txrx: httpd_recv_with_opt: requested length = 128
I (39844) httpd_txrx: httpd_recv_with_opt: received length = 128
I (39844) httpd_parse: read_block: received HTTP request block size = 128
I (39854) httpd_parse: cb_url: message begin
I (39864) httpd_parse: cb_url: processing url = /generate_204
I (39864) httpd_parse: verify_url: received URI = /generate_204
I (39874) httpd_parse: cb_header_field: headers begin
I (39874) httpd_txrx: httpd_unrecv: length = 101
I (39884) httpd_parse: pause_parsing: paused
I (39884) httpd_parse: cb_header_field: processing field = Host
I (39894) httpd_txrx: httpd_recv_with_opt: requested length = 128
I (39904) httpd_txrx: httpd_recv_with_opt: pending length = 101
I (39904) httpd_parse: read_block: received HTTP request block size = 101
I (39914) httpd_parse: continue_parsing: skip pre-parsed data of size = 5
I (39914) httpd_parse: continue_parsing: un-paused
I (39924) httpd_parse: cb_header_value: processing value = hxhwhfpb-ccd-testing-v4.metric.gstatic.com
I (39934) httpd_parse: cb_header_field: processing field = Connection
I (39934) httpd_parse: cb_header_value: processing value = close
I (39944) httpd_parse: cb_header_field: processing field = User-Agent
I (39954) httpd_parse: cb_header_value: processing value = Mozilla/5.0 (X11; CrOS
I (39954) httpd_parse: parse_block: parsed block size = 101
I (39964) httpd_txrx: httpd_recv_with_opt: requested length = 128
I (39974) httpd_txrx: httpd_recv_with_opt: received length = 106
I (39974) httpd_parse: read_block: received HTTP request block size = 106
I (39984) httpd_parse: cb_header_value: processing value =  x86_64 11316.148.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.117 Safari/537.36 CCD:1.1.0
I (39994) httpd_parse: cb_headers_complete: bytes read     = 235
I (40004) httpd_parse: cb_headers_complete: content length = 0
I (40014) httpd_txrx: httpd_unrecv: length = 128
E (40014) httpd_parse: pause_parsing: data too large for un-recv = -2
W (40024) httpd_parse: parse_block: parsing failed
W (40024) httpd_txrx: httpd_resp_send_err: 500 Internal Server Error - Server has encountered an unexpected error
I (40044) httpd_txrx: httpd_send_all: sent = 81
I (40044) httpd_txrx: httpd_send_all: sent = 2
I (40054) httpd_txrx: httpd_send_all: sent = 42
I (40054) httpd: httpd_server: closing socket 62

Looking a bit further in the original crash, the huge buf_len came from parse_parsing, which had this data:

(gdb) p *parser_data
$25 = {
  settings = {
    on_message_begin = 0x0,
    on_url = 0x40176738 <cb_url>,
    on_status = 0x0,
    on_header_field = 0x40176a78 <cb_header_field>,
    on_header_value = 0x401766dc <cb_header_value>,
    on_headers_complete = 0x401768a0 <cb_headers_complete>,
    on_body = 0x40176a20 <cb_on_body>,
    on_message_complete = 0x401769b0 <cb_no_body>,
    on_chunk_header = 0x0,
    on_chunk_complete = 0x0
  },
  req = 0x3ffe6700,
  status = PARSING_BODY,
  error = HTTPD_500_INTERNAL_SERVER_ERROR,
  last = {
    at = 0x3ffe69f1 "",
    length = 126
  },
  paused = false,
  pre_parsed = 4294967294,
  raw_datalen = 207
}
(gdb) p *(struct httpd_req_aux *)parser_data->req->aux
$1 = {
  sd = 0x3ffe7490,
  scratch = "Host: ejntleqg-ccd-testing-v4.metric.gstatic.com\000Connection: close\000User-Agent: Mozilla/5.0 (X11; CrOS x86_64 11316.148.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.117 Safari/537.36 CCD:1.1.0\000\n", '\000' <repeats 1841 times>,
  remaining_len = 0,
  status = 0x3f431524 "200 OK",
  content_type = 0x3f40d624 "text/html",
  first_chunk_sent = false,
  req_hdrs_count = 3,
  resp_hdrs_count = 0,
  resp_hdrs = 0x3ffe7844,
  url_parse_res = {
    field_set = 8,
    port = 0,
    field_data = {{
        off = 0,
        len = 0
      }, {
        off = 0,
        len = 0
      }, {
        off = 0,
        len = 0
      }, {
        off = 0,
        len = 13
      }, {
        off = 0,
        len = 0
      }, {
        off = 0,
        len = 0
      }, {
        off = 0,
        len = 0
      }}
  }
}

Here is the full backtrace at the time:

#0  0x4000c350 in ?? ()
No symbol table info available.
#1  0x40175d50 in httpd_unrecv (r=<optimized out>, buf=0x3ffe69f5 "", buf_len=4294967294) at /fw/esp-idf/components/esp_http_server/src/httpd_txrx.c:160
        ra = 0x3ffe6920
        offset = <optimized out>
#2  0x40176975 in pause_parsing (parser=0x3ffe8cd8, at=0x3ffe69f5 "") at /fw/esp-idf/components/esp_http_server/src/httpd_parse.c:157
        parser_data = 0x3ffe8cf8
        r = <optimized out>
        ra = <optimized out>
        __func__ = "pause_parsing"
#3  0x40176a03 in cb_no_body (parser=0x3ffe8cd8) at /fw/esp-idf/components/esp_http_server/src/httpd_parse.c:389
        parser_data = 0x3ffe8cf8
        at = 0x3ffe69f5 ""
        __func__ = "cb_no_body"
#4  0x4019f362 in http_parser_execute (parser=0x3ffe8cd8, settings=<optimized out>, data=<optimized out>, len=<optimized out>) at /fw/esp-idf/components/nghttp/port/http_parser.c:1870
        hasBody = <optimized out>
        c = <optimized out>
        ch = <optimized out>
        unhex_val = <optimized out>
        p = 0x3ffe69f2 "\n"
        header_field_mark = <optimized out>
        header_value_mark = <optimized out>
        url_mark = <optimized out>
        body_mark = <optimized out>
        status_mark = <optimized out>
        p_state = <optimized out>
        lenient = <optimized out>
        __func__ = "http_parser_execute"
#5  0x40176c16 in parse_block (parser=0x3ffe8cd8, offset=101, length=106) at /fw/esp-idf/components/esp_http_server/src/httpd_parse.c:484
        data = 0x3ffe8cf8
        req = <optimized out>
        raux = 0x3ffe6920
        nparsed = <optimized out>
        __func__ = "parse_block"
#6  0x40176cc4 in httpd_parse_req (hd=0x3ffe66ac) at /fw/esp-idf/components/esp_http_server/src/httpd_parse.c:569
        r = 0x3ffe6700
        blk_len = <optimized out>
        offset = 101
        parser = {
          type = 0, 
          flags = 4, 
          state = 1, 
          header_state = 0, 
          index = 0, 
          lenient_http_headers = 0, 
          nread = 0, 
          content_length = 18446744073709551615, 
          http_major = 1, 
          http_minor = 1, 
          status_code = 0, 
          method = 1, 
          http_errno = 0, 
          upgrade = 0, 
          data = 0x3ffe8cf8
        }
        parser_data = {
          settings = {
            on_message_begin = 0x0, 
            on_url = 0x40176738 <cb_url>, 
            on_status = 0x0, 
            on_header_field = 0x40176a78 <cb_header_field>, 
            on_header_value = 0x401766dc <cb_header_value>, 
            on_headers_complete = 0x401768a0 <cb_headers_complete>, 
            on_body = 0x40176a20 <cb_on_body>, 
            on_message_complete = 0x401769b0 <cb_no_body>, 
            on_chunk_header = 0x0, 
            on_chunk_complete = 0x0
          }, 
          req = 0x3ffe6700, 
          status = PARSING_BODY, 
          error = HTTPD_500_INTERNAL_SERVER_ERROR, 
          last = {
            at = 0x3ffe69f1 "", 
            length = 126
          }, 
          paused = false, 
          pre_parsed = 4294967294, 
          raw_datalen = 207
        }
#7  0x40176d57 in httpd_req_new (hd=0x3ffe66ac, sd=<optimized out>) at /fw/esp-idf/components/esp_http_server/src/httpd_parse.c:645
        r = 0x3ffe6700
        ra = 0x3ffe6920
        err = 1073637036
#8  0x4017717f in httpd_sess_process (hd=0x3ffe66ac, newfd=<optimized out>) at /fw/esp-idf/components/esp_http_server/src/httpd_sess.c:298
        sd = 0x3ffe7490
#9  0x4017562c in httpd_server (hd=0x3ffe66ac) at /fw/esp-idf/components/esp_http_server/src/httpd_main.c:195
        i = 39
        read_set = {
          fds_bits = {0, 67108864}
        }
        tmp_max_fd = 63
        maxfd = <optimized out>
        __func__ = "httpd_server"
        active_cnt = <optimized out>
        fd = 58
#10 0x401756f6 in httpd_thread (arg=0x3ffe66ac) at /fw/esp-idf/components/esp_http_server/src/httpd_main.c:225
        ret = <optimized out>
        hd = 0x3ffe66ac
        __func__ = "httpd_thread"
#11 0x400944dc in vPortTaskWrapper (pxCode=0x401756cc <httpd_thread>, pvParameters=0x3ffe66ac) at /fw/esp-idf/components/freertos/port.c:143
        pcTaskName = 0x401756cc <httpd_thread> "6A"
@github-actions github-actions bot changed the title Crash in esp_http_server request parsing Crash in esp_http_server request parsing (IDFGH-748) Mar 14, 2019
@anurag-kar
Copy link
Contributor

anurag-kar commented Mar 15, 2019

@jimparis Thanks for reporting and thoroughly tracing the issue.

that memcpy should be of length ra->sd->pending_len, right?

You are right, and that needs to be corrected (though as per the parsing logic, the buf_len can never be larger than PARSER_BLOCK_SIZE, which is also the size of array pending_data).

The source of the problem is this line in cb_no_body():

    /* Get end of packet */
    at += strlen("\r\n\r\n");

This assumes that, for packets with no body, the final header will be followed by two CRLFs, though I see your request sends only LFs, that is why this assumption fails:

scratch = "Host: ejntleqg-ccd-testing-v4.metric.gstatic.com\000Connection: close\000User-Agent: Mozilla/5.0 (X11; CrOS x86_64 11316.148.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.117 Safari/537.36 CCD:1.1.0\000\n", '\000' <repeats 1841 times>, 

Initially I was puzzled why the http_parser library (which is used in esp_http_server) doesn't check for CRLFs (as described in the HTTP 1.1 RFC 2616), but it turns out there's a catch :

HTTP/1.1 defines the sequence CR LF as the end-of-line marker for all protocol elements except the entity-body (see appendix 19.3 for tolerant applications).

And as per 19.3

The line terminator for message-header fields is the sequence CRLF. However, we recommend that applications, when parsing such headers, recognize a single LF as a line terminator and ignore the leading CR.

So the parsing logic followed in http_server breaks as this assumption doesn't hold always, for example in this case the browser you are using must be sending out LFs only.

@jimparis
Copy link
Author

Thanks @anurag-kar for finding it. For the record these requests are being generated by Chrome's connectivity diagnostics tool, and I've reported their nonconforming line endings here: https://crbug.com/942528

@jimparis
Copy link
Author

This isn't the world's cleanest or most robust solution but it seems to work here:

diff --git a/components/esp_http_server/src/httpd_parse.c b/components/esp_http_server/src/httpd_parse.c
index 2837778e..f7938c7f 100644
--- a/components/esp_http_server/src/httpd_parse.c
+++ b/components/esp_http_server/src/httpd_parse.c
@@ -52,6 +52,7 @@ typedef struct {
     struct {
         const char *at;
         size_t      length;
+        char        replaced_char;
     } last;
 
     /* State variables */
@@ -217,6 +218,8 @@ static esp_err_t cb_header_field(http_parser *parser, const char *at, size_t len
     } else if (parser_data->status == PARSING_HDR_VALUE) {
         /* NULL terminate last header (key: value) pair */
         size_t offset = parser_data->last.at - ra->scratch;
+        parser_data->last.replaced_char =
+            ra->scratch[offset + parser_data->last.length];
         ra->scratch[offset + parser_data->last.length] = '\0';
 
         /* Store current values of the parser callback arguments */
@@ -290,6 +293,8 @@ static esp_err_t cb_headers_complete(http_parser *parser)
     } else if (parser_data->status == PARSING_HDR_VALUE) {
         /* NULL terminate last header (key: value) pair */
         size_t offset = parser_data->last.at - ra->scratch;
+        parser_data->last.replaced_char =
+            ra->scratch[offset + parser_data->last.length];
         ra->scratch[offset + parser_data->last.length] = '\0';
 
         /* Reach end of last header */
@@ -379,8 +384,11 @@ static esp_err_t cb_no_body(http_parser *parser)
         return ESP_FAIL;
     }
 
-    /* Get end of packet */
-    at += strlen("\r\n\r\n");
+    /* Get end of packet.  Skip over \r\n\r\n, or \n\n  */
+    if (parser_data->last.replaced_char == '\r')
+      at += strlen("\r\n\r\n");
+    else
+      at += strlen("\n\n");
 
     /* Pause parsing so that if part of another packet
      * is in queue then it doesn't get parsed, which

@jimparis
Copy link
Author

And it would also need to support bare LF at the end of header lines with something like this

diff --git a/components/esp_http_server/src/httpd_parse.c b/components/esp_http_server/src/httpd_parse.c
index f7938c7f..54a91d0e 100644
--- a/components/esp_http_server/src/httpd_parse.c
+++ b/components/esp_http_server/src/httpd_parse.c
@@ -844,7 +844,9 @@ size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *field)
          */
         if ((val_ptr - hdr_ptr != strlen(field)) ||
             (strncasecmp(hdr_ptr, field, strlen(field)))) {
-            hdr_ptr += strlen(hdr_ptr) + strlen("\r\n");
+            hdr_ptr += strlen(hdr_ptr) + strlen("\r");
+            if (*hdr_ptr == '\n')
+              hdr_ptr++;
             continue;
         }
         /* Skip ':' */
@@ -890,7 +892,9 @@ esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *field, char *v
          */
         if ((val_ptr - hdr_ptr != strlen(field)) ||
             (strncasecmp(hdr_ptr, field, strlen(field)))) {
-            hdr_ptr += strlen(hdr_ptr) + strlen("\r\n");
+            hdr_ptr += strlen(hdr_ptr) + strlen("\r");
+            if (*hdr_ptr == '\n')
+              hdr_ptr++;
             continue;
         }

@anurag-kar
Copy link
Contributor

For the record these requests are being generated by Chrome's connectivity diagnostics tool, and I've reported their nonconforming line endings here: https://crbug.com/942528

That's great!

@jimparis I really appreciate your efforts in solving this issue, though we have already started working on the bugfix (along with tests for checking support for LF/CRLF, so that we don't end up breaking this delicate logic in the future), but I am pretty sure it will not be very different from the logic that you are using in your version of the fix. Hope to have it merged as soon as possible. Till then please bear with us and thank you for cooperating.

@igrr igrr closed this as completed in 990af31 Apr 1, 2019
igrr pushed a commit that referenced this issue Apr 2, 2019
List of changes:
* When parsing requests, count termination from LF characters only
* Correct memcpy() length parameter in httpd_unrecv() (pointed out by jimparis in GitHub issue thread)
* Use ssize_t to store results of length subtractions during parsing
* Modify some comments to reduce ambiguity

Closes #3182
igrr pushed a commit that referenced this issue May 27, 2019
List of changes:
* When parsing requests, count termination from LF characters only
* Correct memcpy() length parameter in httpd_unrecv() (pointed out by jimparis in GitHub issue thread)
* Use ssize_t to store results of length subtractions during parsing
* Modify some comments to reduce ambiguity

Closes #3182
loganfin pushed a commit to Lumenaries/esp_http_server that referenced this issue Apr 23, 2024
List of changes:
* When parsing requests, count termination from LF characters only
* Correct memcpy() length parameter in httpd_unrecv() (pointed out by jimparis in GitHub issue thread)
* Use ssize_t to store results of length subtractions during parsing
* Modify some comments to reduce ambiguity

Closes espressif/esp-idf#3182
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants