Fossil SCM

http.c: Complete the HTTP 1.1 chunked reply decoder Fix error conditions and exceptions when Transfer-Encoding chunked, partly from drh's Claude: * Premature EOF was accepted as a valid EOF. * strtol --> strtoll with checking, to avoid casting errors * Per RFC 7230, match "chunked" at the end of the Transfer-Encoding value rather than a substring, so the word "chunked" appearing isn't acceped as a token * make nPrior unsigned int to match blob_size() to avoid a negative offset on large replies * Report only the chunk bytes actually received, not chunk bytes from the header, so the number is correct if the connection terminates part-way.

danshearer 2026-06-13 09:58 UTC http1-1-chunked
Commit 441a35ce2a1f63c0a85521cd709da43a35294a7555a0ba598180280cb7fc27f1
1 file changed +44 -17
+44 -17
--- src/http.c
+++ src/http.c
@@ -608,11 +608,13 @@
608608
closeConnection = 0;
609609
}else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){
610610
for(i=15; fossil_isspace(zLine[i]); i++){}
611611
iLength = atoi(&zLine[i]);
612612
}else if( fossil_strnicmp(zLine, "transfer-encoding:", 18)==0 ){
613
- if( sqlite3_strlike("%chunked%", &zLine[18], 0)==0 ){
613
+ /* RFC 7230: "chunked" must be the final transfer-coding so only
614
+ ** match when it appears at the end of the line. */
615
+ if( sqlite3_strlike("%chunked", &zLine[18], 0)==0 ){
614616
isChunked = 1;
615617
}
616618
}else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){
617619
if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){
618620
closeConnection = 1;
@@ -748,37 +750,62 @@
748750
** chunk is a hex length on its own line (optionally followed by a
749751
** ";extension" that is ignored), then that many payload bytes, then a
750752
** bare CRLF. A zero-length chunk terminates the body, after which any
751753
** trailer header lines are read and discarded up to the blank line. */
752754
char *zChunk;
753
- while( (zChunk = transport_receive_line(&g.url))!=0 ){
754
- int nChunk; /* Size of this chunk in bytes */
755
- int nPrior; /* Bytes already in pReply */
755
+ int sawTerminator = 0; /* True once the 0-length chunk is seen */
756
+ while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){
757
+ sqlite3_int64 nChunk; /* Size of this chunk in bytes (wide, unclamped) */
758
+ unsigned int nPrior; /* Bytes already in pReply (matches blob nUsed) */
759
+ char *zEnd = 0; /* End of the hex digits actually parsed */
756760
while( fossil_isspace(zChunk[0]) ) zChunk++;
757
- nChunk = (int)strtol(zChunk, 0, 16);
758
- if( nChunk<0 ){
761
+ nChunk = strtoll(zChunk, &zEnd, 16);
762
+ if( zEnd==zChunk ){
763
+ /* No hex digit consumed: a blank or malformed chunk-size line, which
764
+ ** is the symptom of a connection that closed mid-stream. Treat it as
765
+ ** a truncated (failed) */
766
+ fossil_warning("chunked reply: missing or malformed chunk size");
767
+ goto write_err;
768
+ }
769
+ if( nChunk<0 || nChunk>0x7fffffff ){
770
+ /* Negative, or larger than we will ever accept in one chunk. */
771
+ fossil_warning("chunked reply: invalid chunk size");
759772
goto write_err;
760773
}
761774
if( nChunk==0 ){
762775
/* Final chunk: consume trailer lines up to the terminating blank. */
776
+ sawTerminator = 1;
763777
while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){}
764778
break;
765779
}
766780
nPrior = blob_size(pReply);
767
- blob_resize(pReply, nPrior+nChunk);
768
- /* transport_receive() may return short; loop until the chunk is full. */
769
- while( nChunk>0 ){
770
- int nGot = transport_receive(&g.url, &pReply->aData[nPrior], nChunk);
771
- if( nGot<=0 ){
772
- fossil_warning("chunked reply truncated");
773
- goto write_err;
774
- }
775
- nPrior += nGot;
776
- nChunk -= nGot;
777
- pReply->nUsed = nPrior;
781
+ /* Reserve space without advancing nUsed, so that on error
782
+ ** the blob's reported size equals the bytes actually
783
+ ** received rather the claimed chunk length */
784
+ blob_reserve(pReply, nPrior+(unsigned int)nChunk);
785
+ {
786
+ unsigned int nRemaining = (unsigned int)nChunk;
787
+ /* transport_receive() may return short; loop until the chunk is full. */
788
+ while( nRemaining>0 ){
789
+ int nGot = transport_receive(&g.url, &pReply->aData[nPrior], nRemaining);
790
+ if( nGot<=0 ){
791
+ fossil_warning("chunked reply truncated");
792
+ goto write_err;
793
+ }
794
+ nPrior += (unsigned int)nGot;
795
+ nRemaining -= (unsigned int)nGot;
796
+ pReply->nUsed = nPrior;
797
+ }
778798
}
779799
transport_receive_line(&g.url); /* CRLF that follows the chunk data */
800
+ }
801
+ if( !sawTerminator ){
802
+ /* The loop exited without ever seeing the 0-length terminator chunk,
803
+ ** meaning the peer closed before the body was complete. A truncated
804
+ ** sync must be an error, never silent success. */
805
+ fossil_warning("chunked reply ended without terminator");
806
+ goto write_err;
780807
}
781808
}else if( iLength==0 ){
782809
/* No content to read */
783810
}else if( iLength>0 ){
784811
/* Read content of a known length */
785812
--- src/http.c
+++ src/http.c
@@ -608,11 +608,13 @@
608 closeConnection = 0;
609 }else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){
610 for(i=15; fossil_isspace(zLine[i]); i++){}
611 iLength = atoi(&zLine[i]);
612 }else if( fossil_strnicmp(zLine, "transfer-encoding:", 18)==0 ){
613 if( sqlite3_strlike("%chunked%", &zLine[18], 0)==0 ){
 
 
614 isChunked = 1;
615 }
616 }else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){
617 if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){
618 closeConnection = 1;
@@ -748,37 +750,62 @@
748 ** chunk is a hex length on its own line (optionally followed by a
749 ** ";extension" that is ignored), then that many payload bytes, then a
750 ** bare CRLF. A zero-length chunk terminates the body, after which any
751 ** trailer header lines are read and discarded up to the blank line. */
752 char *zChunk;
753 while( (zChunk = transport_receive_line(&g.url))!=0 ){
754 int nChunk; /* Size of this chunk in bytes */
755 int nPrior; /* Bytes already in pReply */
 
 
756 while( fossil_isspace(zChunk[0]) ) zChunk++;
757 nChunk = (int)strtol(zChunk, 0, 16);
758 if( nChunk<0 ){
 
 
 
 
 
 
 
 
 
759 goto write_err;
760 }
761 if( nChunk==0 ){
762 /* Final chunk: consume trailer lines up to the terminating blank. */
 
763 while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){}
764 break;
765 }
766 nPrior = blob_size(pReply);
767 blob_resize(pReply, nPrior+nChunk);
768 /* transport_receive() may return short; loop until the chunk is full. */
769 while( nChunk>0 ){
770 int nGot = transport_receive(&g.url, &pReply->aData[nPrior], nChunk);
771 if( nGot<=0 ){
772 fossil_warning("chunked reply truncated");
773 goto write_err;
774 }
775 nPrior += nGot;
776 nChunk -= nGot;
777 pReply->nUsed = nPrior;
 
 
 
 
 
 
778 }
779 transport_receive_line(&g.url); /* CRLF that follows the chunk data */
 
 
 
 
 
 
 
780 }
781 }else if( iLength==0 ){
782 /* No content to read */
783 }else if( iLength>0 ){
784 /* Read content of a known length */
785
--- src/http.c
+++ src/http.c
@@ -608,11 +608,13 @@
608 closeConnection = 0;
609 }else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){
610 for(i=15; fossil_isspace(zLine[i]); i++){}
611 iLength = atoi(&zLine[i]);
612 }else if( fossil_strnicmp(zLine, "transfer-encoding:", 18)==0 ){
613 /* RFC 7230: "chunked" must be the final transfer-coding so only
614 ** match when it appears at the end of the line. */
615 if( sqlite3_strlike("%chunked", &zLine[18], 0)==0 ){
616 isChunked = 1;
617 }
618 }else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){
619 if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){
620 closeConnection = 1;
@@ -748,37 +750,62 @@
750 ** chunk is a hex length on its own line (optionally followed by a
751 ** ";extension" that is ignored), then that many payload bytes, then a
752 ** bare CRLF. A zero-length chunk terminates the body, after which any
753 ** trailer header lines are read and discarded up to the blank line. */
754 char *zChunk;
755 int sawTerminator = 0; /* True once the 0-length chunk is seen */
756 while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){
757 sqlite3_int64 nChunk; /* Size of this chunk in bytes (wide, unclamped) */
758 unsigned int nPrior; /* Bytes already in pReply (matches blob nUsed) */
759 char *zEnd = 0; /* End of the hex digits actually parsed */
760 while( fossil_isspace(zChunk[0]) ) zChunk++;
761 nChunk = strtoll(zChunk, &zEnd, 16);
762 if( zEnd==zChunk ){
763 /* No hex digit consumed: a blank or malformed chunk-size line, which
764 ** is the symptom of a connection that closed mid-stream. Treat it as
765 ** a truncated (failed) */
766 fossil_warning("chunked reply: missing or malformed chunk size");
767 goto write_err;
768 }
769 if( nChunk<0 || nChunk>0x7fffffff ){
770 /* Negative, or larger than we will ever accept in one chunk. */
771 fossil_warning("chunked reply: invalid chunk size");
772 goto write_err;
773 }
774 if( nChunk==0 ){
775 /* Final chunk: consume trailer lines up to the terminating blank. */
776 sawTerminator = 1;
777 while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){}
778 break;
779 }
780 nPrior = blob_size(pReply);
781 /* Reserve space without advancing nUsed, so that on error
782 ** the blob's reported size equals the bytes actually
783 ** received rather the claimed chunk length */
784 blob_reserve(pReply, nPrior+(unsigned int)nChunk);
785 {
786 unsigned int nRemaining = (unsigned int)nChunk;
787 /* transport_receive() may return short; loop until the chunk is full. */
788 while( nRemaining>0 ){
789 int nGot = transport_receive(&g.url, &pReply->aData[nPrior], nRemaining);
790 if( nGot<=0 ){
791 fossil_warning("chunked reply truncated");
792 goto write_err;
793 }
794 nPrior += (unsigned int)nGot;
795 nRemaining -= (unsigned int)nGot;
796 pReply->nUsed = nPrior;
797 }
798 }
799 transport_receive_line(&g.url); /* CRLF that follows the chunk data */
800 }
801 if( !sawTerminator ){
802 /* The loop exited without ever seeing the 0-length terminator chunk,
803 ** meaning the peer closed before the body was complete. A truncated
804 ** sync must be an error, never silent success. */
805 fossil_warning("chunked reply ended without terminator");
806 goto write_err;
807 }
808 }else if( iLength==0 ){
809 /* No content to read */
810 }else if( iLength>0 ){
811 /* Read content of a known length */
812

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button