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.
>
> 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 12 Jul 2026 13:09:31 -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,11 +794,11 @@ 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;
> - char *p, *start_str, *end_str;
> - const char *errstr;
> + char *p, *ep, *start_str, *end_str;
> + unsigned long long value;
>
> if ((p = strchr(str, '-')) == NULL)
> return (0);
> @@ -802,27 +813,46 @@ 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);
> + errno = 0;
> + value = strtoull(end_str, &ep, 10);
> + if (*ep != '\0' || value == 0)
> + return (0);
> + if (errno == ERANGE || value >= (unsigned long long)size)
> + r->start = 0;
> + else
> + r->start = size - (off_t)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);
> + errno = 0;
> + value = strtoull(start_str, &ep, 10);
> + if (*ep != '\0' || errno == ERANGE ||
> + value >= (unsigned long long)size)
> + return (0);
> + r->start = (off_t)value;
>
> - 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;
> + errno = 0;
> + value = strtoull(end_str, &ep, 10);
> + if (*ep != '\0')
> + return (0);
> + if (errno == ERANGE || value >= (unsigned long long)size)
> + r->end = size - 1;
> + else
> + r->end = (off_t)value;
> + } 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)
> Index: regress/usr.sbin/httpd/tests/Makefile
> ===================================================================
> RCS file: /home/cvs/src/regress/usr.sbin/httpd/tests/Makefile,v
> diff -u -p -r1.16 Makefile
> --- regress/usr.sbin/httpd/tests/Makefile 22 Dec 2021 15:54:01 -0000
> 1.16
> +++ regress/usr.sbin/httpd/tests/Makefile 12 Jul 2026 12:22:57 -0000
> @@ -50,6 +50,8 @@ run-$a: $a ${HTDOCS_MD5}
> time SUDO="${SUDO}" KTRACE=${KTRACE} HTTPD=${HTTPD} perl ${PERLINC}
> ${PERLPATH}httpd.pl ${.OBJDIR} ${PERLPATH}$a
> .endfor
>
> +run-args-get-range-suffix.pl: htdocs/range htdocs/empty htdocs/416.html
> +
> # populate htdocs
>
> .for d in ${HTDOCS_FILES}
> @@ -64,6 +66,18 @@ htdocs/$d:
> md5-$d: htdocs/$d
> md5 -q htdocs/$d >$@
> .endfor
> +
> +htdocs/range: range-content
> + mkdir -m 0755 -p ${@:H}
> + cp ${.ALLSRC} $@
> +
> +htdocs/empty:
> + mkdir -m 0755 -p ${@:H}
> + : >$@
> +
> +htdocs/416.html: range-error
> + mkdir -m 0755 -p ${@:H}
> + cp ${.ALLSRC} $@
>
> # create certificates for TLS
>
> Index: regress/usr.sbin/httpd/tests/args-get-range-suffix.pl
> ===================================================================
> RCS file: regress/usr.sbin/httpd/tests/args-get-range-suffix.pl
> diff -N regress/usr.sbin/httpd/tests/args-get-range-suffix.pl
> --- /dev/null 1 Jan 1970 00:00:00 -0000
> +++ regress/usr.sbin/httpd/tests/args-get-range-suffix.pl 12 Jul 2026
> 12:38:53 -0000
> @@ -0,0 +1,109 @@
> +use strict;
> +use warnings;
> +
> +use Digest::MD5 qw(md5_hex);
> +
> +my $content = "0123456789\n";
> +my $error = "range error\n";
> +my @cases = (
> + {
> + path => "range",
> + range => "bytes=-5",
> + code => "206 Partial Content",
> + body => "6789\n",
> + },
> + {
> + path => "range",
> + range => "bytes=-11",
> + code => "206 Partial Content",
> + body => $content,
> + },
> + {
> + path => "range",
> + range => "bytes=-15",
> + code => "206 Partial Content",
> + body => $content,
> + },
> + {
> + path => "range",
> + range => "bytes=-0",
> + code => "416 Range Not Satisfiable",
> + body => $error,
> + },
> + {
> + path => "empty",
> + range => "bytes=-1",
> + code => "416 Range Not Satisfiable",
> + body => $error,
> + },
> + {
> + path => "range",
> + range => "bytes=-4294967296",
> + code => "206 Partial Content",
> + body => $content,
> + },
> + {
> + path => "range",
> + range => "bytes=-9223372036854775808",
> + code => "206 Partial Content",
> + body => $content,
> + },
> + {
> + path => "range",
> + range => "bytes=0-9223372036854775808",
> + code => "206 Partial Content",
> + body => $content,
> + },
> + {
> + path => "range",
> + range => "bytes=-18446744073709551616",
> + code => "206 Partial Content",
> + body => $content,
> + },
> + {
> + path => "range",
> + range => "bytes=0-18446744073709551616",
> + code => "206 Partial Content",
> + body => $content,
> + },
> + {
> + path => "range",
> + range => "bytes=--1",
> + code => "416 Range Not Satisfiable",
> + body => $error,
> + },
> +);
> +my @lengths = map { length($_->{body}) } @cases;
> +my @md5 = map { md5_hex($_->{body}) } @cases;
> +
> +our %args = (
> + client => {
> + func => sub {
> + my $self = shift;
> + my $case = shift @cases;
> +
> + $self->{path} = $case->{path};
> + $self->{header}{Range} = $case->{range};
> + $self->{code} = $case->{code};
> + http_request($self, length($case->{body}), "1.0");
> + http_response($self, length($case->{body}));
> + $self->{redo} = @cases;
> + },
> + loggrep => [
> + { qr/^<<< Content-Range: bytes 6-10\/11$/ => 1 },
> + { qr/^<<< Content-Range: bytes 0-10\/11$/ => 7 },
> + { qr/^<<< Content-Range: bytes \*\/11$/ => 2 },
> + { qr/^<<< Content-Range: bytes \*\/0$/ => 1 },
> + { qr/^<<< Content-Length: 5$/ => 1 },
> + { qr/^<<< Content-Length: 11$/ => 7 },
> + { qr/^<<< Content-Length: 12$/ => 3 },
> + ],
> + },
> + httpd => {
> + http => 'errdocs "/"',
> + },
> + lengths => \@lengths,
> + md5 => \@md5,
> +);
> +
> +1;
> Index: regress/usr.sbin/httpd/tests/range-content
> ===================================================================
> RCS file: regress/usr.sbin/httpd/tests/range-content
> diff -N regress/usr.sbin/httpd/tests/range-content
> --- /dev/null 1 Jan 1970 00:00:00 -0000
> +++ regress/usr.sbin/httpd/tests/range-content 12 Jul 2026 12:22:57
> -0000
> @@ -0,0 +1 @@
> +0123456789
> Index: regress/usr.sbin/httpd/tests/range-error
> ===================================================================
> RCS file: regress/usr.sbin/httpd/tests/range-error
> diff -N regress/usr.sbin/httpd/tests/range-error
> --- /dev/null 1 Jan 1970 00:00:00 -0000
> +++ regress/usr.sbin/httpd/tests/range-error 12 Jul 2026 12:22:57 -0000
> @@ -0,0 +1 @@
> +range error
>
>
> --
> wbr, Kirill
>