Re: httpd: HTTP suffix-range requests omits file contents
Rafael Sadowski <[email protected]> Sun, 19 Jul 2026 06:01:07 +0200
| Newsgroups | gmane.os.openbsd.bugs |
|---|---|
| Message-ID | <[email protected]> |
On Sat Jul 18, 2026 at 07:27:07PM +0200, Kirill A. Korinsky wrote: > On Sat, 18 Jul 2026 15:25:00 +0200, > Rafael Sadowski <[email protected]> wrote: > > > > On Sun Jul 12, 2026 at 03:15:15PM +0200, Kirill A. Korinsky wrote: > > > On Sat, 04 Jul 2026 00:27:00 +0200, > > > Andrew Kloet <[email protected]> wrote: > > > > > > > > On June 18, 2026 7:44:31 PM UTC, Andrew Kloet <[email protected]> wrote: > > > > >>Synopsis: HTTP suffix-range requests omits file contents > > > > >>Category: user > > > > >>Environment: > > > > > System : OpenBSD 7.9 > > > > > Details : OpenBSD 7.9 (GENERIC.MP) #449: Wed May 6 13:17:25 MDT 2026 > > > > > [email protected]:/usr/src/sys/arch/amd64/compile/GENERIC.MP > > > > > > > > > > Architecture: OpenBSD.amd64 > > > > > Machine : amd64 > > > > >>Description: > > > > > There is a logic flaw inside httpd's parse_range_sepc when > > > > > processing HTTP Byte-Range Requests. > > > > > > > > > > When a client issues a suffix-range request (Range: bytes=-num to > > > > > request the trailing num bytes of a resource) where the requested > > > > > count is larger than the target file size, an overflow guard caps > > > > > r->end to size - 1 prematurely. > > > > > > > > > > Because this truncation happens before the evaluation block > > > > > calculates the starting byte offset (r->start = size - r->end), the > > > > > resulting offset math evaluates to 1 instead of 0. As a result, > > > > > httpd serves a 206 Partial Content response that completely omits > > > > > the very first byte (offset 0) of the target file. > > > > >>How-To-Repeat: > > > > > # Create an 11-byte file > > > > > $ echo "0123456789" > /var/www/htdocs/test > > > > > > > > > > # Send an HTTP request with a suffix range exceeding the file size. > > > > > # Note the response headers show an incorrect range and omit the > > > > > # leading character > > > > > $ curl -v -H "Range: bytes=-15" http://127.0.0.1/test > > > > > < HTTP/1.1 206 Partial Content > > > > > < Content-Range: bytes 1-10/11 > > > > > < Content-Length: 10 > > > > > 123456789 > > > > >>Fix: > > > > > Decouple the suffix range processing from standard ranges within > > > > > parse_range_spec so that ceiling adjustments to r->end don't corrupt > > > > > the initial offset computation. > > > > > > > > > >--- a/usr.sbin/httpd/server_file.c > > > > >+++ b/usr.sbin/httpd/server_file.c > > > > >@@ -797,15 +797,18 @@ parse_range_spec(char *str, size_t size, struct range *r) > > > > > if ((start_str_len == 0) && (end_str_len == 0)) > > > > > return (0); > > > > > > > > > >- if (end_str_len) { > > > > >+ if (start_str_len == 0) { > > > > > r->end = strtonum(end_str, 0, LLONG_MAX, &errstr); > > > > >- if (errstr) > > > > >+ if (errstr || r->end == 0) > > > > > return (0); > > > > > > > > > >- if ((size_t)r->end >= size) > > > > >- r->end = size - 1; > > > > >- } else > > > > >+ if ((size_t)r->end > size) > > > > >+ r->start = 0; > > > > >+ else > > > > >+ r->start = size - r->end; > > > > > r->end = size - 1; > > > > >+ return (1); > > > > >+ } > > > > > > > > > > if (start_str_len) { > > > > > r->start = strtonum(start_str, 0, LLONG_MAX, &errstr); > > > > >@@ -814,11 +817,17 @@ parse_range_spec(char *str, size_t size, struct range *r) > > > > > > > > > > if ((size_t)r->start >= size) > > > > > return (0); > > > > >- } else { > > > > >- r->start = size - r->end; > > > > >- r->end = size - 1; > > > > > } > > > > > > > > > >+ if (end_str_len) { > > > > >+ r->end = strtonum(end_str, 0, LLONG_MAX, &errstr); > > > > >+ if (errstr) > > > > >+ return (0); > > > > >+ if ((size_t)r->end >= size) > > > > >+ r->end = size - 1; > > > > >+ } else > > > > >+ r->end = size - 1; > > > > >+ > > > > > if (r->end < r->start) > > > > > return (0); > > > > > > > > bump > > > > > > > > > > I think you have fixed only one edge case. After spending some time on this, > > > I had this diff which fixed a few more cases plus addresses tests. > > > > > > Here, I calculate suffix ranges before clamping end offsets so requests at > > > least as large as the file return the complete representation. Reject zero > > > length and malformed ranges, retain 64 bit sizes throughout range handling, > > > and add regression coverage for boundaries and overflow. > > > > > > Ok? > > > > A thought on this. You could keep the strtonum and compare the errstr > > strcmp(errstr, "too large"). I think that would make the code easier to > > read and you safe the errno dance, but that's just my personal opinion. > > > > It’s up to you to decide, OK rsadowski if you want go with this version. > > > > You're right, that make that error dance simpler. > > Ok? OK rsadowski > > Index: usr.sbin/httpd/httpd.h > =================================================================== > RCS file: /home/cvs/src/usr.sbin/httpd/httpd.h,v > diff -u -p -r1.178 httpd.h > --- usr.sbin/httpd/httpd.h 2 Jul 2026 04:59:16 -0000 1.178 > +++ usr.sbin/httpd/httpd.h 12 Jul 2026 12:19:37 -0000 > @@ -280,7 +280,7 @@ struct range_data { > > /* For the Content headers in each part */ > struct media_type *range_media; > - size_t range_total; > + off_t range_total; > }; > > struct client { > Index: usr.sbin/httpd/server_file.c > =================================================================== > RCS file: /home/cvs/src/usr.sbin/httpd/server_file.c,v > diff -u -p -r1.82 server_file.c > --- usr.sbin/httpd/server_file.c 21 Jun 2026 19:23:56 -0000 1.82 > +++ usr.sbin/httpd/server_file.c 18 Jul 2026 17:17:51 -0000 > @@ -52,8 +52,8 @@ int server_file_index(struct httpd *, > int server_file_modified_since(struct http_descriptor *, > const struct timespec *); > int server_file_method(struct client *); > -int parse_range_spec(char *, size_t, struct range *); > -int parse_ranges(struct client *, char *, size_t); > +int parse_range_spec(char *, off_t, struct range *); > +int parse_ranges(struct client *, char *, off_t); > static int select_visible(const struct dirent *); > > int > @@ -351,7 +351,8 @@ server_partial_file_request(struct httpd > struct media_type multipart_media; > struct range_data *r = &clt->clt_ranges; > struct range *range; > - size_t content_length = 0, bufsiz; > + off_t content_length = 0, range_length; > + size_t bufsiz; > int code = 500, i, nranges, ret; > char content_range[64]; > const char *errstr = NULL; > @@ -396,12 +397,22 @@ server_partial_file_request(struct httpd > goto abort; > > /* Add data length */ > - content_length += ret + range->end - range->start + 1; > + range_length = range->end - range->start + 1; > + if (content_length > LLONG_MAX - ret || > + range_length > LLONG_MAX - content_length - ret) { > + errno = EOVERFLOW; > + goto abort; > + } > + content_length += ret + range_length; > > } > if ((ret = snprintf(NULL, 0, "\r\n--%llu--\r\n", > clt->clt_boundary)) < 0) > goto abort; > + if (content_length > LLONG_MAX - ret) { > + errno = EOVERFLOW; > + goto abort; > + } > content_length += ret; > > /* prepare multipart/byteranges media type */ > @@ -743,7 +754,7 @@ server_file_modified_since(struct http_d > } > > int > -parse_ranges(struct client *clt, char *str, size_t file_sz) > +parse_ranges(struct client *clt, char *str, off_t file_sz) > { > int i = 0; > char *p, *q; > @@ -783,9 +794,10 @@ parse_ranges(struct client *clt, char *s > } > > int > -parse_range_spec(char *str, size_t size, struct range *r) > +parse_range_spec(char *str, off_t size, struct range *r) > { > size_t start_str_len, end_str_len; > + off_t value; > char *p, *start_str, *end_str; > const char *errstr; > > @@ -802,27 +814,41 @@ parse_range_spec(char *str, size_t size, > if ((start_str_len == 0) && (end_str_len == 0)) > return (0); > > - if (end_str_len) { > - r->end = strtonum(end_str, 0, LLONG_MAX, &errstr); > - if (errstr) > - return (0); > + if (size == 0) > + return (0); > > - if ((size_t)r->end >= size) > - r->end = size - 1; > - } else > + if (start_str_len == 0) { > + if (*end_str < '0' || *end_str > '9') > + return (0); > + value = strtonum(end_str, 0, size, &errstr); > + if (errstr != NULL && errno != ERANGE) > + return (0); > + if (errstr != NULL) > + r->start = 0; > + else if (value == 0) > + return (0); > + else > + r->start = size - value; > r->end = size - 1; > + return (1); > + } > > - if (start_str_len) { > - r->start = strtonum(start_str, 0, LLONG_MAX, &errstr); > - if (errstr) > - return (0); > + if (*start_str < '0' || *start_str > '9') > + return (0); > + r->start = strtonum(start_str, 0, size - 1, &errstr); > + if (errstr != NULL) > + return (0); > > - if ((size_t)r->start >= size) > + if (end_str_len) { > + if (*end_str < '0' || *end_str > '9') > return (0); > - } else { > - r->start = size - r->end; > + r->end = strtonum(end_str, 0, size - 1, &errstr); > + if (errstr != NULL && errno != ERANGE) > + return (0); > + if (errstr != NULL) > + r->end = size - 1; > + } else > r->end = size - 1; > - } > > if (r->end < r->start) > return (0); > Index: usr.sbin/httpd/server_http.c > =================================================================== > RCS file: /home/cvs/src/usr.sbin/httpd/server_http.c,v > diff -u -p -r1.167 server_http.c > --- usr.sbin/httpd/server_http.c 2 Jul 2026 04:59:50 -0000 1.167 > +++ usr.sbin/httpd/server_http.c 12 Jul 2026 13:04:29 -0000 > @@ -724,7 +724,7 @@ server_read_httprange(struct bufferevent > if (server_bufferevent_printf(clt, > "\r\n--%llu\r\n" > "Content-Type: %s/%s\r\n" > - "Content-Range: bytes %lld-%lld/%zu\r\n\r\n", > + "Content-Range: bytes %lld-%lld/%lld\r\n\r\n", > clt->clt_boundary, > media->media_type, media->media_subtype, > range->start, range->end, r->range_total) == -1) > > > -- > wbr, Kirill >