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
> 

Reply via email to