Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 10, 22:30 UTC
caddyhttp: fix min_rate credit overflow on transfers over 9.2 GB (#8396)
IdleDeadline.next computed the min_rate allowance as transferred * time.Second / MinRate, which overflows int64 once a request body or response passes about 9.2 GB. The credit goes negative, the deadline lands in the past, and the connection is cut even though the client is far above the configured rate. Compute the credit with a 128-bit intermediate and saturate instead.
Jae Birdsall committed
Nov 10, 2026 at 09:57 UTC
f458aa0081f55445b09fae9799d40ed98ed4b3b7
2 files changed
+59
-2
modules/caddyhttp/idletimeout.go
+26
-2
@@ -18,6 +18,8 @@ import (
18
"context"
19
"errors"
20
"io"
21
+ "math"
22
+ "math/bits"
23
"net/http"
24
"os"
25
"sync"
@@ -64,8 +66,11 @@ type IdleDeadline struct {
66
67
func (d *IdleDeadline) next() (deadline time.Time) {
68
if d.MinRate > 0 {
67
- credit := time.Duration(d.transferred) * time.Second / time.Duration(d.MinRate)
68
- deadline = d.Start.Add(d.Timeout + credit)
69
+ allowance := d.Timeout + rateCredit(d.transferred, d.MinRate)
70
+ if allowance < d.Timeout { // the sum overflowed
71
+ allowance = math.MaxInt64
72
+ }
73
+ deadline = d.Start.Add(allowance)
74
} else {
75
deadline = time.Now().Add(d.Timeout)
76
}
@@ -76,6 +81,25 @@ func (d *IdleDeadline) next() (deadline time.Time) {
81
return
82
}
83
84
+// rateCredit returns how long transferring n bytes may take at rate
85
+// bytes/second, saturating instead of overflowing: n * time.Second exceeds
86
+// int64 once n passes about 9.2 GB, which would turn the credit negative
87
+// and abort large transfers that easily sustain the rate.
88
+func rateCredit(n, rate int64) time.Duration {
89
+ if n <= 0 {
90
+ return 0
91
+ }
92
+ hi, lo := bits.Mul64(uint64(n), uint64(time.Second))
93
+ if hi >= uint64(rate) {
94
+ return math.MaxInt64
95
+ }
96
+ q, _ := bits.Div64(hi, lo, uint64(rate))
97
+ if q > math.MaxInt64 {
98
+ return math.MaxInt64
99
+ }
100
+ return time.Duration(q)
101
+}
102
+
103
// idleOverride holds what restore needs to undo an override.
104
type idleOverride struct {
105
prev IdleDeadline
modules/caddyhttp/idletimeout_test.go
+33
@@ -20,6 +20,7 @@ import (
20
"errors"
21
"fmt"
22
"io"
23
+ "math"
24
"net/http"
25
"net/http/httptest"
26
"os"
@@ -1605,3 +1606,35 @@ func TestIdleTimeoutWriterRestoresHardDeadline(t *testing.T) {
1606
})
1607
}
1608
}
1609
+
1610
+func TestIdleDeadlineMinRateLargeTransfer(t *testing.T) {
1611
+ start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
1612
+ const gb = 1_000_000_000
1613
+ for _, tc := range []struct {
1614
+ name string
1615
+ transferred int64
1616
+ minRate int64
1617
+ want time.Duration // allowance past start, excluding Timeout
1618
+ }{
1619
+ {"small", 5 * 1024 * 1024, 1024 * 1024, 5 * time.Second},
1620
+ {"just below overflow", 9 * gb, gb, 9 * time.Second},
1621
+ {"10 GB at 1 MB/s", 10 * gb, 1_000_000, 10_000 * time.Second},
1622
+ {"100 GB at 1 GB/s", 100 * gb, gb, 100 * time.Second},
1623
+ {"1 TB at 1 B/s", 1000 * gb, 1, math.MaxInt64},
1624
+ {"max int64 bytes", math.MaxInt64, 1, math.MaxInt64},
1625
+ } {
1626
+ t.Run(tc.name, func(t *testing.T) {
1627
+ d := IdleDeadline{Start: start, Timeout: time.Minute, MinRate: tc.minRate, transferred: tc.transferred}
1628
+ got := d.next()
1629
+ if tc.want == math.MaxInt64 {
1630
+ if !got.After(start.Add(100 * 365 * 24 * time.Hour)) {
1631
+ t.Fatalf("deadline = %v, want far in the future", got)
1632
+ }
1633
+ return
1634
+ }
1635
+ if want := start.Add(time.Minute + tc.want); !got.Equal(want) {
1636
+ t.Fatalf("deadline = %v, want %v", got, want)
1637
+ }
1638
+ })
1639
+ }
1640
+}