Hi,
thanks. I'll have a look at it, I would be happy if this provides a
way to get rid of calling the bev->readcb callbacks again.
Reyk
On Tue, May 12, 2015 at 05:34:58PM -0400, Bertrand PROVOST wrote:
> Hi,
>
> I found a crash in relayd when using http relay. `bev` pointer is
> used after a free in `relay_http.c` lines: 438, 492 and 609
>
> In `relay_http.c` there is 3 functions, used as read callback for
> libevent buffer:
> * `relay_read_http`: parse http headers
> * `relay_read_httpcontent`: parse simple http content
> * `relay_read_httpchunks`: parse http content sent using 'chunked'
> method
>
> When one of the three functions is finishing its work and data are sill
> available in the buffer , the function calls `bev->readcb(bev, arg);`
> to handle the remaining data. This last action is mandatory, because
> these remaining data would have been read from the socket and are in
> the the current bufferevent Libevent will not call the callback anymore,
> as a result the request will timeout.
>
> This breaks the callback designs and leads to bugy software.
>
> A crash occurs when the connection is closed.
> In this case, the context had freed inside the callback.
> The callback return no information as requested by libevent's design.
> Nevertheless the context is used just afer.
>
> For example:
>
> The function `relay_read_httpchunks` finish to read all
> chunk of data for the current request, but there is still data
> remaining, so it directly call `relay_read_http` to parse the next HTTP
> request, with the following code:
>
> if (EVBUFFER_LENGTH(src))
> bev->readcb(bev, arg);
>
> The problem is that inside these 3 functions, if an error occure the
> functions `relay_close` or `relay_abort_http` are called, and then it
> free all data related to the current connection. Then after this line:
>
> bev->readcb(bev, arg);
>
> `bev` has been free, and the following line which is:
>
> bufferevent_enable(bev, EV_READ);
>
> cause a crash (SIGBUS/SIGSEGV), when trying to accesss to one of the
> field of `bev`
>
>
> Please review the following patch that do not manually call the
> callback. Moreover this implementation explicitly shows the state
> machine that was hidden inside the libevent context data.
>
> The new callback has 3 states just like there was 3 callbacks before.
> The callback calls the previous processing and check if more processing
> must be done before calling `bufferevent_enable`
>
> So the functions:
> * `relay_read_http`
> * `relay_read_httpcontent`
> * `relay_read_httpchunks`
>
> now returns:
> * -1 if datas had been free
> * 0 if all is OK, and it should try to parse remaining data
> * 1 if data is OK, and it should not try to parse remaining data
>
>
> Index: http.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/relayd/http.h,v
> retrieving revision 1.5
> diff -u -p -r1.5 http.h
> --- http.h 14 Aug 2014 10:30:52 -0000 1.5
> +++ http.h 12 May 2015 13:14:21 -0000
> @@ -180,6 +180,14 @@ struct http_mediatype {
> { NULL } \
> }
>
> +/* Define state of current http relay */
> +enum httpstate {
> + HTTP_STATE_READ_HEADER = 0,
> + HTTP_STATE_READ_CONTENT,
> + HTTP_STATE_READ_CHUNKS,
> + HTTP_STATE_READ_DATA,
> +};
> +
> /* Used during runtime */
> struct http_descriptor {
> struct kv http_pathquery;
> @@ -202,6 +210,8 @@ struct http_descriptor {
> /* A tree of headers and attached lists for repeated headers. */
> struct kv *http_lastheader;
> struct kvtree http_headers;
> +
> + enum httpstate http_state;
> };
>
> #endif /* _HTTP_H */
> Index: relay.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/relayd/relay.c,v
> retrieving revision 1.193
> diff -u -p -r1.193 relay.c
> --- relay.c 29 Apr 2015 08:41:24 -0000 1.193
> +++ relay.c 12 May 2015 13:14:21 -0000
> @@ -683,7 +683,7 @@ relay_connected(int fd, short sig, void
> return;
> }
> con->se_out.toread = TOREAD_HTTP_HEADER;
> - outrd = relay_read_http;
> + outrd = relay_read_http_cb;
> break;
> case RELAY_PROTO_TCP:
> /* Use defaults */
> @@ -734,7 +734,7 @@ relay_input(struct rsession *con)
> return;
> }
> con->se_in.toread = TOREAD_HTTP_HEADER;
> - inrd = relay_read_http;
> + inrd = relay_read_http_cb;
> break;
> case RELAY_PROTO_TCP:
> /* Use defaults */
> Index: relay_http.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/relayd/relay_http.c,v
> retrieving revision 1.44
> diff -u -p -r1.44 relay_http.c
> --- relay_http.c 29 Apr 2015 08:41:24 -0000 1.44
> +++ relay_http.c 12 May 2015 13:14:21 -0000
> @@ -49,8 +49,9 @@ int relay_lookup_url(struct ctl_relay_
> int relay_lookup_query(struct ctl_relay_event *, struct kv *);
> int relay_lookup_cookie(struct ctl_relay_event *, const char *,
> struct kv *);
> -void relay_read_httpcontent(struct bufferevent *, void *);
> -void relay_read_httpchunks(struct bufferevent *, void *);
> +int relay_read_http(struct bufferevent *, void *);
> +int relay_read_httpcontent(struct bufferevent *, void *);
> +int relay_read_httpchunks(struct bufferevent *, void *);
> char *relay_expand_http(struct ctl_relay_event *, char *,
> char *, size_t);
> int relay_writeheader_kv(struct ctl_relay_event *, struct kv *);
> @@ -151,7 +152,53 @@ relay_httpdesc_free(struct http_descript
> kv_purge(&desc->http_headers);
> }
>
> -void
> +void relay_read_http_cb(struct bufferevent *bev, void *arg) {
> + struct ctl_relay_event *cre = arg;
> + struct http_descriptor *desc = cre->desc;
> + struct rsession *con = cre->con;
> + struct evbuffer *src = EVBUFFER_INPUT(bev);
> + int rc = 0;
> + int stop = 0;
> + int try_splice = 0;
> +
> + do {
> + switch (desc->http_state) {
> + case HTTP_STATE_READ_HEADER:
> + rc = relay_read_http(bev, arg);
> + if (rc != -1 && desc->http_state == HTTP_STATE_READ_HEADER) {
> + // The state does not change, so it can stop here
> + // this mean the http headers has not been fully parsed
> + // because there is not enough data available
> + stop = 1;
> + try_splice = 1;
> + }
> + break;
> + case HTTP_STATE_READ_CONTENT:
> + rc = relay_read_httpcontent(bev, arg);
> + if (rc != -1 && desc->http_state == HTTP_STATE_READ_CONTENT) {
> + // The state does not change, so it can stop here
> + stop = 1;
> + try_splice = 0;
> + }
> + break;
> + case HTTP_STATE_READ_CHUNKS:
> + rc = relay_read_httpchunks(bev, arg);
> + try_splice = 0;
> + break;
> + case HTTP_STATE_READ_DATA:
> + relay_read(bev, arg);
> + return;
> + }
> + } while (!stop && rc == 0 && EVBUFFER_LENGTH(src));
> +
> + if (rc == 0) {
> + bufferevent_enable(bev, EV_READ);
> + if (try_splice && relay_splice(cre) == -1)
> + relay_close(con, strerror(errno));
> + }
> +}
> +
> +int
> relay_read_http(struct bufferevent *bev, void *arg)
> {
> struct ctl_relay_event *cre = arg;
> @@ -174,7 +221,7 @@ relay_read_http(struct bufferevent *bev,
> __func__, con->se_id, size, cre->toread);
> if (!size) {
> if (cre->dir == RELAY_DIR_RESPONSE)
> - return;
> + return 1;
> cre->toread = TOREAD_HTTP_HEADER;
> goto done;
> }
> @@ -198,7 +245,7 @@ relay_read_http(struct bufferevent *bev,
> if (cre->headerlen > RELAY_MAXHEADERLENGTH) {
> free(line);
> relay_abort_http(con, 413, "request too large", 0);
> - return;
> + return -1;
> }
>
> /*
> @@ -216,7 +263,7 @@ relay_read_http(struct bufferevent *bev,
> if (cre->line == 1) {
> free(line);
> relay_abort_http(con, 400, "malformed", 0);
> - return;
> + return -1;
> }
>
> /* Append line to the last header, if present */
> @@ -354,23 +401,23 @@ relay_read_http(struct bufferevent *bev,
> if (cre->done) {
> if (desc->http_method == HTTP_METHOD_NONE) {
> relay_abort_http(con, 406, "no method", 0);
> - return;
> + return -1;
> }
>
> action = relay_test(proto, cre);
> if (action == RES_FAIL) {
> relay_close(con, "filter rule failed");
> - return;
> + return -1;
> } else if (action != RES_PASS) {
> relay_abort_http(con, 403, "Forbidden", con->se_label);
> - return;
> + return -1;
> }
>
> switch (desc->http_method) {
> case HTTP_METHOD_CONNECT:
> /* Data stream */
> cre->toread = TOREAD_UNLIMITED;
> - bev->readcb = relay_read;
> + desc->http_state = HTTP_STATE_READ_DATA;
> break;
> case HTTP_METHOD_DELETE:
> case HTTP_METHOD_GET:
> @@ -383,24 +430,24 @@ relay_read_http(struct bufferevent *bev,
> case HTTP_METHOD_RESPONSE:
> /* HTTP request payload */
> if (cre->toread > 0)
> - bev->readcb = relay_read_httpcontent;
> + desc->http_state = HTTP_STATE_READ_CONTENT;
>
> /* Single-pass HTTP body */
> if (cre->toread < 0) {
> cre->toread = TOREAD_UNLIMITED;
> - bev->readcb = relay_read;
> + desc->http_state = HTTP_STATE_READ_DATA;
> }
> break;
> default:
> /* HTTP handler */
> cre->toread = TOREAD_HTTP_HEADER;
> - bev->readcb = relay_read_http;
> + desc->http_state = HTTP_STATE_READ_HEADER;
> break;
> }
> if (desc->http_chunked) {
> /* Chunked transfer encoding */
> cre->toread = TOREAD_HTTP_CHUNK_LENGTH;
> - bev->readcb = relay_read_httpchunks;
> + desc->http_state = HTTP_STATE_READ_CHUNKS;
> }
>
> if (cre->dir == RELAY_DIR_REQUEST) {
> @@ -421,35 +468,32 @@ relay_read_http(struct bufferevent *bev,
> cre->dst->bev == NULL) {
> if (rlay->rl_conf.fwdmode == FWD_TRANS) {
> relay_bindanyreq(con, 0, IPPROTO_TCP);
> - return;
> + return -1;
> }
> if (relay_connect(con) == -1) {
> relay_abort_http(con, 502, "session failed", 0);
> - return;
> + return -1;
> }
> }
> }
> if (con->se_done) {
> relay_close(con, "last http read (done)");
> - return;
> + return -1;
> }
> - if (EVBUFFER_LENGTH(src) && bev->readcb != relay_read_http)
> - bev->readcb(bev, arg);
> - bufferevent_enable(bev, EV_READ);
> - if (relay_splice(cre) == -1)
> - relay_close(con, strerror(errno));
> - return;
> + return 0;
> fail:
> relay_abort_http(con, 500, strerror(errno), 0);
> - return;
> + return -1;
> abort:
> free(line);
> + return -1;
> }
>
> -void
> +int
> relay_read_httpcontent(struct bufferevent *bev, void *arg)
> {
> struct ctl_relay_event *cre = arg;
> + struct http_descriptor *desc = cre->desc;
> struct rsession *con = cre->con;
> struct evbuffer *src = EVBUFFER_INPUT(bev);
> size_t size;
> @@ -461,7 +505,7 @@ relay_read_httpcontent(struct buffereven
> DPRINTF("%s: session %d: size %lu, to read %lld", __func__,
> con->se_id, size, cre->toread);
> if (!size)
> - return;
> + return 1;
> if (relay_spliceadjust(cre) == -1)
> goto fail;
>
> @@ -483,25 +527,24 @@ relay_read_httpcontent(struct buffereven
> }
> if (cre->toread == 0) {
> cre->toread = TOREAD_HTTP_HEADER;
> - bev->readcb = relay_read_http;
> + desc->http_state = HTTP_STATE_READ_HEADER;
> }
> if (con->se_done)
> goto done;
> - if (bev->readcb != relay_read_httpcontent)
> - bev->readcb(bev, arg);
> - bufferevent_enable(bev, EV_READ);
> - return;
> + return 0;
> done:
> relay_close(con, "last http content read");
> - return;
> + return -1;
> fail:
> relay_close(con, strerror(errno));
> + return -1;
> }
>
> -void
> +int
> relay_read_httpchunks(struct bufferevent *bev, void *arg)
> {
> struct ctl_relay_event *cre = arg;
> + struct http_descriptor *desc = cre->desc;
> struct rsession *con = cre->con;
> struct evbuffer *src = EVBUFFER_INPUT(bev);
> char *line;
> @@ -515,7 +558,7 @@ relay_read_httpchunks(struct bufferevent
> DPRINTF("%s: session %d: size %lu, to read %lld", __func__,
> con->se_id, size, cre->toread);
> if (!size)
> - return;
> + return 1;
> if (relay_spliceadjust(cre) == -1)
> goto fail;
>
> @@ -541,7 +584,7 @@ relay_read_httpchunks(struct bufferevent
> if (line == NULL) {
> /* Ignore empty line, continue */
> bufferevent_enable(bev, EV_READ);
> - return;
> + return 1;
> }
> if (strlen(line) == 0) {
> free(line);
> @@ -555,7 +598,7 @@ relay_read_httpchunks(struct bufferevent
> if (sscanf(line, "%llx", &llval) != 1 || llval < 0) {
> free(line);
> relay_close(con, "invalid chunk size");
> - return;
> + return -1;
> }
>
> if (relay_bufferevent_print(cre->dst, line) == -1 ||
> @@ -576,7 +619,7 @@ relay_read_httpchunks(struct bufferevent
> if (line == NULL) {
> /* Ignore empty line, continue */
> bufferevent_enable(bev, EV_READ);
> - return;
> + return -1;
> }
> if (relay_bufferevent_print(cre->dst, line) == -1 ||
> relay_bufferevent_print(cre->dst, "\r\n") == -1) {
> @@ -586,7 +629,7 @@ relay_read_httpchunks(struct bufferevent
> if (strlen(line) == 0) {
> /* Switch to HTTP header mode */
> cre->toread = TOREAD_HTTP_HEADER;
> - bev->readcb = relay_read_http;
> + desc->http_state = HTTP_STATE_READ_HEADER;
> }
> free(line);
> break;
> @@ -604,16 +647,14 @@ relay_read_httpchunks(struct bufferevent
> next:
> if (con->se_done)
> goto done;
> - if (EVBUFFER_LENGTH(src))
> - bev->readcb(bev, arg);
> - bufferevent_enable(bev, EV_READ);
> - return;
> + return 0;
>
> done:
> relay_close(con, "last http chunk read (done)");
> - return;
> + return -1;
> fail:
> relay_close(con, strerror(errno));
> + return -1;
> }
>
> void
> Index: relayd.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/relayd/relayd.h,v
> retrieving revision 1.209
> diff -u -p -r1.209 relayd.h
> --- relayd.h 2 May 2015 13:15:24 -0000 1.209
> +++ relayd.h 12 May 2015 13:14:21 -0000
> @@ -1184,7 +1184,7 @@ void relay_http(struct relayd *);
> void relay_http_init(struct relay *);
> void relay_abort_http(struct rsession *, u_int, const char *,
> u_int16_t);
> -void relay_read_http(struct bufferevent *, void *);
> +void relay_read_http_cb(struct bufferevent *, void *);
> void relay_close_http(struct rsession *);
> u_int relay_httpmethod_byname(const char *);
> const char
>
>
>
> --
> Bertrand PROVOST
>
--