Bug #121677 libcno accepts requests whose final transfer coding is not chunked

Submitted: 5 Nov 13:51
Reporter: QiFan Liu Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Router Severity:S3 (Non-critical)
Version:561753af986c3c006c4b6c7caa8359748fb55b22 OS:Linux
Assigned to: CPU Architecture:x86
Tags: HTTP/1.1, libcno, MySQL Router, request framing, Transfer-Encoding

[5 Nov 13:51] QiFan Liu
Description:
MySQL Router's bundled libcno parser reports successful completion for HTTP/1.1 requests whose final transfer coding is not chunked. This was reproduced with Transfer-Encoding: gzip and Transfer-Encoding: chunked, gzip, using both contiguous input and input fragmented one byte at a time.

Decisive observation:
The valid Transfer-Encoding: chunked controls reached the message-tail callback and passed. Both invalid transfer-coding chains also returned rc=0 and reached that callback in both input modes:

valid_chunked fragmented=0 rc=0 heads=1 tails=1 oracle=PASS
invalid_nonfinal_chunked fragmented=0 rc=0 heads=1 tails=1 oracle=FAIL
invalid_missing_chunked fragmented=0 rc=0 heads=1 tails=1 oracle=FAIL
valid_chunked fragmented=1 rc=0 heads=1 tails=1 oracle=PASS
invalid_nonfinal_chunked fragmented=1 rc=0 heads=1 tails=1 oracle=FAIL
invalid_missing_chunked fragmented=1 rc=0 heads=1 tails=1 oracle=FAIL
oracle_failures=4

Semantic mismatch:
RFC 9112 section 6.3 requires a server to reject and close a request when the final transfer coding is not chunked. The invalid cases should therefore fail before the message-tail callback.

The demonstrated runtime behavior is parser-level acceptance. Source inspection shows that Router's ServerConnection routes a request from the message-tail callback and does not add validation of the transfer-coding chain, so the accepted malformed framing can proceed to that routing path. An end-to-end Router HTTP response and application dispatch were not exercised.

Impact:
The affected scope is Router HTTP/1 endpoints backed by the bundled libcno parser. mysqld's SQL protocol is unaffected. Closing the affected HTTP connection is sufficient recovery. The reproducer creates no service or database state.

Tested versions:
MySQL source revision 561753af986c3c006c4b6c7caa8359748fb55b22.

Environment and configuration:
Linux on amd64. The harness builds the bundled libcno sources directly and uses the parser in HTTP/1 server mode with HTTP/2 prior knowledge disabled. No running Router service or non-default server configuration is required for this parser-level reproduction.

Additional information:
This occurs when a Router HTTP/1 request uses an unsupported transfer coding or places another coding after chunked.

How to repeat:
Use a MySQL source checkout at revision 561753af986c3c006c4b6c7caa8359748fb55b22. Save the following two files in an otherwise empty directory.

case.c:

#include <stdio.h>
#include <string.h>
#include "cno/core.h"

struct observations { int heads, tails; };
static int head(void *p, uint32_t id, const struct cno_message_t *m) { (void)id; (void)m; ((struct observations *)p)->heads++; return 0; }
static int tail(void *p, uint32_t id, const struct cno_tail_t *t) { (void)id; (void)t; ((struct observations *)p)->tails++; return 0; }
static int data(void *p, uint32_t id, const char *d, size_t n) { (void)p; (void)id; (void)d; (void)n; return 0; }
static const struct cno_vtable_t callbacks = { .on_message_head=head, .on_message_data=data, .on_message_tail=tail };

static int run(const char *label, const char *te, int fragmented, int valid) {
  char request[512];
  snprintf(request, sizeof(request), "POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: %s\r\n\r\n0\r\n\r\n", te);
  struct cno_connection_t c; struct observations o = {0};
  cno_init(&c, CNO_SERVER); c.cb_code=&callbacks; c.cb_data=&o; c.disallow_h2_prior_knowledge=1;
  int rc=cno_begin(&c, CNO_HTTP1);
  for (size_t pos=0; rc==0 && pos<strlen(request);) { size_t n=fragmented ? 1 : strlen(request)-pos; rc=cno_consume(&c, request+pos, n); pos+=n; }
  int accepted=rc==0 && o.heads==1 && o.tails==1;
  int pass=valid ? accepted : (rc<0 && o.tails==0);
  printf("%s fragmented=%d rc=%d heads=%d tails=%d oracle=%s\n", label, fragmented, rc, o.heads, o.tails, pass?"PASS":"FAIL");
  cno_fini(&c); return !pass;
}
int main(void) {
  int failures=0;
  for (int fragmented=0; fragmented<=1; fragmented++) {
    failures+=run("valid_chunked", "chunked", fragmented, 1);
    failures+=run("invalid_nonfinal_chunked", "chunked, gzip", fragmented, 0);
    failures+=run("invalid_missing_chunked", "gzip", fragmented, 0);
  }
  printf("oracle_failures=%d\n", failures);
  return failures ? 1 : 0;
}

run.sh:

#!/bin/sh
set -eu
if [ "$#" -ne 1 ]; then
  echo "usage: $0 /path/to/mysql-source" >&2
  exit 2
fi
src=$1
here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
vendor="$src/extra/libcno"
cno="$vendor/libcno-208939f540957a35b337dacdd5c5e34d51821bd2"
build=$(mktemp -d "${TMPDIR:-/tmp}/mysql-router-te-repro.XXXXXX")
c++ -std=c++17 -O1 "$vendor/cno_huffman_generator.cc" -o "$build/generator"
"$build/generator" "$build/hpack-data.h"
cc -std=gnu11 -O1 -I"$cno" -I"$build" "$here/case.c" "$cno/cno/core.c" "$cno/cno/common.c" "$cno/cno/hpack.c" "$cno/picohttpparser/picohttpparser.c" -o "$build/case"
"$build/case"

Run:

chmod +x run.sh
./run.sh /path/to/mysql-source

The recorded output is:

valid_chunked fragmented=0 rc=0 heads=1 tails=1 oracle=PASS
invalid_nonfinal_chunked fragmented=0 rc=0 heads=1 tails=1 oracle=FAIL
invalid_missing_chunked fragmented=0 rc=0 heads=1 tails=1 oracle=FAIL
valid_chunked fragmented=1 rc=0 heads=1 tails=1 oracle=PASS
invalid_nonfinal_chunked fragmented=1 rc=0 heads=1 tails=1 oracle=FAIL
invalid_missing_chunked fragmented=1 rc=0 heads=1 tails=1 oracle=FAIL
oracle_failures=4

The process exits with status 1. With a corrected parser, both invalid cases should return an error before the tail callback for contiguous and fragmented input, oracle_failures should be 0, and the process should exit with status 0.

Suggested fix:
No patch proposed.

Portions of this website are copyright © 2001, 2002 The PHP Group

Page generated in 0.038 sec. using MySQL 8.4.7-u6-cloud

Timestamp references displayed by the system are UTC.
Content reproduced on this site is the property of the respective copyright holders.
It is not reviewed in advance by Oracle and does not necessarily represent the opinion of Oracle or any other party.