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?

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