Files
coder/provisionersdk/proto/dataupload.go
T
Garrett Delfosse a2e1ddb56f fix: validate FileSize in NewDataBuilder to prevent OOM DoS (#25710)
`NewDataBuilder` allocated `make([]byte, 0, req.FileSize)` using the
client-supplied `int64` with no upper-bound check. The DRPC 4 MiB wire
cap limits message size but not the integer value, so a crafted message
with `FileSize = 1<<40` forces a 1 TiB allocation, triggering an
unrecoverable `runtime.throw` that kills the entire `coderd` process.

Add a `MaxFileSize` constant (100 MiB, matching `HTTPFileMaxBytes` in
`coderd/files.go`) and reject negative or oversized `FileSize`, plus
negative or excessive `Chunks`, before the allocation.
`BytesToDataUpload` also returns an error for oversized data to preserve
the encode/decode round-trip contract. Fix a pre-existing reversed
subtraction in the `Add()` overflow error message.

Closes https://linear.app/codercom/issue/PLAT-231

<details>
<summary>Implementation details</summary>

- `provisionersdk/proto/dataupload.go`: New exported `MaxFileSize`
constant; validation in `NewDataBuilder` and `BytesToDataUpload`. Fixed
reversed subtraction in `Add()` error.
- `provisionersdk/proto/dataupload_test.go`: New
`TestNewDataBuilderValidation` with 7 subtests.
- Updated all 5 callers of `BytesToDataUpload` for new error return.
- Audited all `make([]byte, ...)` in provisioner paths; no other
client-supplied sizes.

</details>

> Generated by Coder Agents on behalf of @f0ssel
2026-05-27 14:30:11 -04:00

160 lines
4.3 KiB
Go
Generated

package proto
import (
"bytes"
"crypto/sha256"
"sync"
"golang.org/x/xerrors"
)
const (
ChunkSize = 2 << 20 // 2 MiB
MaxFileSize = 10 * (10 << 20) // 100 MiB, matches coderd HTTPFileMaxBytes
)
type DataBuilder struct {
Type DataUploadType
Hash []byte
Size int64
ChunkCount int32
// chunkIndex is the index of the next chunk to be added.
chunkIndex int32
mu sync.Mutex
data []byte
}
func NewDataBuilder(req *DataUpload) (*DataBuilder, error) {
if len(req.DataHash) != 32 {
return nil, xerrors.Errorf("data hash must be 32 bytes, got %d bytes", len(req.DataHash))
}
if req.FileSize < 0 {
return nil, xerrors.Errorf("file size must not be negative, got %d", req.FileSize)
}
if req.FileSize > MaxFileSize {
return nil, xerrors.Errorf("file size %d exceeds maximum allowed %d", req.FileSize, MaxFileSize)
}
if req.Chunks < 0 {
return nil, xerrors.Errorf("chunk count must not be negative, got %d", req.Chunks)
}
//nolint:gosec // FileSize is validated to be <= MaxFileSize, well within int32 range
maxChunks := int32((req.FileSize + ChunkSize - 1) / ChunkSize)
if req.Chunks > maxChunks {
return nil, xerrors.Errorf("chunk count %d exceeds maximum %d for file size %d", req.Chunks, maxChunks, req.FileSize)
}
return &DataBuilder{
Type: req.UploadType,
Hash: req.DataHash,
Size: req.FileSize,
ChunkCount: req.Chunks,
// Initial conditions
chunkIndex: 0,
data: make([]byte, 0, req.FileSize),
}, nil
}
func (b *DataBuilder) Add(chunk *ChunkPiece) (bool, error) {
b.mu.Lock()
defer b.mu.Unlock()
if !bytes.Equal(b.Hash, chunk.FullDataHash) {
return b.done(), xerrors.Errorf("data hash does not match, this chunk is for a different data upload")
}
if b.done() {
return b.done(), xerrors.Errorf("data upload is already complete, cannot add more chunks")
}
if chunk.PieceIndex != b.chunkIndex {
return b.done(), xerrors.Errorf("chunks ordering, expected chunk index %d, got %d", b.chunkIndex, chunk.PieceIndex)
}
expectedSize := len(b.data) + len(chunk.Data)
if expectedSize > int(b.Size) {
return b.done(), xerrors.Errorf("data exceeds expected size, data is now %d bytes, %d bytes over the limit of %d",
expectedSize, int64(expectedSize)-b.Size, b.Size)
}
b.data = append(b.data, chunk.Data...)
b.chunkIndex++
return b.done(), nil
}
// IsDone is always safe to call
func (b *DataBuilder) IsDone() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.done()
}
func (b *DataBuilder) Complete() ([]byte, error) {
b.mu.Lock()
defer b.mu.Unlock()
if !b.done() {
return nil, xerrors.Errorf("data upload is not complete, expected %d chunks, got %d", b.ChunkCount, b.chunkIndex)
}
if len(b.data) != int(b.Size) {
return nil, xerrors.Errorf("data size mismatch, expected %d bytes, got %d bytes", b.Size, len(b.data))
}
hash := sha256.Sum256(b.data)
if !bytes.Equal(hash[:], b.Hash) {
return nil, xerrors.Errorf("data hash mismatch, expected %x, got %x", b.Hash, hash[:])
}
// A safe method would be to return a copy of the data, but that would have to
// allocate double the memory. Just return the original slice, and let the caller
// handle the memory management.
return b.data, nil
}
func (b *DataBuilder) done() bool {
return b.chunkIndex >= b.ChunkCount
}
func BytesToDataUpload(dataType DataUploadType, data []byte) (*DataUpload, []*ChunkPiece, error) {
if int64(len(data)) > MaxFileSize {
return nil, nil, xerrors.Errorf("data size %d exceeds maximum allowed %d", len(data), MaxFileSize)
}
fullHash := sha256.Sum256(data)
//nolint:gosec // not going over int32
size := int32(len(data))
// basically ceiling division to get the number of chunks required to
// hold the data, each chunk is ChunkSize bytes.
chunkCount := (size + ChunkSize - 1) / ChunkSize
req := &DataUpload{
DataHash: fullHash[:],
FileSize: int64(size),
Chunks: chunkCount,
UploadType: dataType,
}
chunks := make([]*ChunkPiece, 0, chunkCount)
for i := int32(0); i < chunkCount; i++ {
start := int64(i) * ChunkSize
end := start + ChunkSize
if end > int64(size) {
end = int64(size)
}
chunkData := data[start:end]
chunk := &ChunkPiece{
PieceIndex: i,
Data: chunkData,
FullDataHash: fullHash[:],
}
chunks = append(chunks, chunk)
}
return req, chunks, nil
}