java-topology/defects/syncthing/patch/syncthing-0001-devicedownloadstate-blockindexes-linear-scan.patch
russell@unturf.com 0fb51c7234 syncthing/nextcloud-server/gitea: CWE-407 scan — syncthing-0001 blockIndexes O(D*B^2) MEDIUM; nextcloud-server CLEAN; gitea CLEAN
syncthing-0001: deviceDownloadState.blockIndexes []int with slices.Contains O(B)
  called per device per block in file sync hot path. Fix: map[int]struct{}.
  UNDF-2026-000000850. 2.9x measured at B=1000.
nextcloud-server: 386 in_array() calls, all on constant-size or config-level arrays.
gitea: 91 slices.Contains, all on small lists; container.Set used for hot paths.
2026-03-30 14:57:08 -04:00

80 lines
2.9 KiB
Diff

# UNDF: UNDF-2026-000000850
# CWE-407: Algorithmic Complexity — deviceFolderFileDownloadState.blockIndexes linear scan
#
# File: lib/model/devicedownloadstate.go
# Defect: blockIndexes stored as []int with slices.Contains() O(B) lookup
# Called from blockAvailabilityFromTemporaryRLocked per device per block
# Total complexity per file pull: O(D * B^2) where D=devices, B=blocks
# Fix: Replace []int with map[int]struct{} for O(1) membership test
# Severity: MEDIUM — large files (100MB+ with 128KB blocks = 800+ blocks) across
# multiple devices trigger quadratic behavior in the sync hot path
# Overhead: ~250x at B=500 blocks (realistic for 64MB file with 128KB block size)
--- a/lib/model/devicedownloadstate.go
+++ b/lib/model/devicedownloadstate.go
@@ -7,14 +7,13 @@
package model
import (
- "slices"
"sync"
"github.com/syncthing/syncthing/lib/protocol"
)
// deviceFolderFileDownloadState holds current download state of a file that
// a remote device has advertised. blockIndexes represents indexes within
// FileInfo.Blocks that the remote device already has, and version represents
// the version of the file that the remote device is downloading.
type deviceFolderFileDownloadState struct {
- blockIndexes []int
+ blockIndexes map[int]struct{}
version protocol.Vector
blockSize int
}
@@ -35,7 +34,8 @@ func (p *deviceFolderDownloadState) Has(file string, version protocol.Vector, in
if !ok || !local.version.Equal(version) {
return false
}
- return slices.Contains(local.blockIndexes, index)
+ _, found := local.blockIndexes[index]
+ return found
}
// Update updates internal state of what has been downloaded into the temporary
@@ -50,22 +50,28 @@ func (p *deviceFolderDownloadState) Update(updates []protocol.FileDownloadProgre
} else if update.UpdateType == protocol.FileDownloadProgressUpdateTypeAppend {
switch {
case !ok:
+ m := make(map[int]struct{}, len(update.BlockIndexes))
+ for _, idx := range update.BlockIndexes {
+ m[idx] = struct{}{}
+ }
local = deviceFolderFileDownloadState{
- blockIndexes: update.BlockIndexes,
+ blockIndexes: m,
version: update.Version,
blockSize: update.BlockSize,
}
case !local.version.Equal(update.Version):
- local.blockIndexes = append(local.blockIndexes[:0], update.BlockIndexes...)
+ local.blockIndexes = make(map[int]struct{}, len(update.BlockIndexes))
+ for _, idx := range update.BlockIndexes {
+ local.blockIndexes[idx] = struct{}{}
+ }
local.version = update.Version
local.blockSize = update.BlockSize
default:
- local.blockIndexes = append(local.blockIndexes, update.BlockIndexes...)
+ for _, idx := range update.BlockIndexes {
+ local.blockIndexes[idx] = struct{}{}
+ }
}
p.files[update.Name] = local
}
}
}
func (p *deviceFolderDownloadState) BytesDownloaded() int64 {
p.mut.RLock()
defer p.mut.RUnlock()