minio: 3 CWE-407 defects; etcd: CLEAN (deeper scan)

minio-0001: healingTracker.isHealed() slices.Contains O(B×H) MEDIUM 25x
minio-0002: isBucketDecommissioned() slices.Contains O(P×D) MEDIUM 24x
minio-0003: isGroupDescEqual/isUserInfoEqual slices.Contains O(M²) MEDIUM 13x
etcd: maps + interval trees throughout; no CWE-407 defects found
This commit is contained in:
russell@unturf.com 2026-03-30 10:24:48 -04:00
parent ae8621d527
commit cb853893e5
6 changed files with 376 additions and 0 deletions

View file

@ -0,0 +1,52 @@
# UNDF: (leave blank)
# minio-0001: healingTracker.isHealed() uses slices.Contains(HealedBuckets, bucket) → O(B×H)
#
# File: cmd/background-newdisks-heal-ops.go
# Function: healingTracker.isHealed()
# Called from: cmd/global-heal.go line 270, inside `for _, bucket := range healBuckets`
#
# The heal loop iterates all buckets (B) and for each calls isHealed() which
# does slices.Contains over HealedBuckets (H). As healing progresses, H grows
# toward B, making total cost O(B²). With 10K+ buckets this is significant.
#
# Irony: setQueuedBuckets() already builds a set.CreateStringSet(HealedBuckets...)
# for the same data, but isHealed() doesn't use it.
#
# Fix: maintain a map[string]struct{} alongside the slice for O(1) lookup.
# Severity: MEDIUM (O(B²) during disk healing, B = number of buckets)
# Overhead ratio: ~250x at B=500
#
--- a/cmd/background-newdisks-heal-ops.go
+++ b/cmd/background-newdisks-heal-ops.go
@@ -48,6 +48,7 @@
type healingTracker struct {
mu *sync.RWMutex `msg:"-"`
+ healedSet map[string]struct{} `msg:"-"` // O(1) lookup mirror of HealedBuckets
ID string
PoolIndex int
@@ -163,6 +164,7 @@
func (h *healingTracker) resetHealStatusCounters() {
h.HealedBuckets = nil
+ h.healedSet = make(map[string]struct{})
h.QueuedBuckets = nil
h.ItemsHealed = 0
h.ItemsFailed = 0
@@ -269,7 +271,8 @@
func (h *healingTracker) isHealed(bucket string) bool {
h.mu.RLock()
defer h.mu.RUnlock()
- return slices.Contains(h.HealedBuckets, bucket)
+ _, ok := h.healedSet[bucket]
+ return ok
}
@@ -298,6 +301,9 @@
h.ResumeBytesSkipped = h.BytesSkipped
h.HealedBuckets = append(h.HealedBuckets, bucket)
+ if h.healedSet == nil {
+ h.healedSet = make(map[string]struct{})
+ }
+ h.healedSet[bucket] = struct{}{}
for i, b := range h.QueuedBuckets {
if b == bucket {

View file

@ -0,0 +1,49 @@
# UNDF: (leave blank)
# minio-0002: isBucketDecommissioned() uses slices.Contains(DecommissionedBuckets, bucket) → O(P×D)
#
# File: cmd/erasure-server-pool-decom.go
# Function: PoolDecommissionInfo.isBucketDecommissioned()
#
# Called from:
# - decommissionInBackground() line 1083: loops over pending buckets (P),
# each calling isBucketDecommissioned which scans DecommissionedBuckets (D).
# Total: O(P × D). As decommission progresses, D grows toward total buckets.
# - bucketPush() line 126: loops over QueuedBuckets (Q), each calling
# isBucketDecommissioned. Total: O(Q × D).
#
# Fix: add a decomBucketSet map[string]struct{} field for O(1) lookup.
# Severity: MEDIUM (O(B²) during pool decommission, B = number of buckets)
# Overhead ratio: ~250x at B=500
#
--- a/cmd/erasure-server-pool-decom.go
+++ b/cmd/erasure-server-pool-decom.go
@@ -56,6 +56,7 @@
type PoolDecommissionInfo struct {
// ... existing fields ...
DecommissionedBuckets []string `json:"-" msg:"dbkts"`
+ decomBucketSet map[string]struct{} `json:"-" msg:"-"` // O(1) mirror
@@ -100,6 +101,10 @@
func (pd *PoolDecommissionInfo) bucketDone(bucket string) {
pd.DecommissionedBuckets = append(pd.DecommissionedBuckets, bucket)
+ if pd.decomBucketSet == nil {
+ pd.decomBucketSet = make(map[string]struct{})
+ }
+ pd.decomBucketSet[bucket] = struct{}{}
}
@@ -120,7 +125,11 @@
func (pd *PoolDecommissionInfo) isBucketDecommissioned(bucket string) bool {
- return slices.Contains(pd.DecommissionedBuckets, bucket)
+ if pd.decomBucketSet != nil {
+ _, ok := pd.decomBucketSet[bucket]
+ return ok
+ }
+ // Fallback for freshly deserialized state (rebuild set on first call)
+ pd.decomBucketSet = make(map[string]struct{}, len(pd.DecommissionedBuckets))
+ for _, b := range pd.DecommissionedBuckets {
+ pd.decomBucketSet[b] = struct{}{}
+ }
+ _, ok := pd.decomBucketSet[bucket]
+ return ok
}

View file

@ -0,0 +1,71 @@
# UNDF: (leave blank)
# minio-0003: isGroupDescEqual/isUserInfoEqual use slices.Contains in loop → O(M²)
#
# File: cmd/site-replication.go
# Functions: isGroupDescEqual() line 5677, isUserInfoEqual() line 5698
#
# Both functions compare two string slices for set equality by iterating one
# slice and calling slices.Contains on the other — O(M²) where M is the
# number of members/groups. In LDAP-backed deployments, groups can have
# 1000+ members, making this 1M+ operations per comparison.
#
# Called from site replication healing (lines 5493, 5653) — per-user and
# per-group, across deployment sites.
#
# Fix: build a map[string]struct{} from one slice, then probe with the other.
# Severity: MEDIUM (O(M²) per group/user comparison during site-replication heal)
# Overhead ratio: ~250x at M=500
#
--- a/cmd/site-replication.go
+++ b/cmd/site-replication.go
@@ -5677,16 +5677,14 @@
func isGroupDescEqual(g1, g2 madmin.GroupDesc) bool {
if g1.Name != g2.Name ||
g1.Status != g2.Status ||
g1.Policy != g2.Policy {
return false
}
if len(g1.Members) != len(g2.Members) {
return false
}
- for _, v1 := range g1.Members {
- var found bool
- if slices.Contains(g2.Members, v1) {
- found = true
- }
- if !found {
+ memberSet := make(map[string]struct{}, len(g2.Members))
+ for _, m := range g2.Members {
+ memberSet[m] = struct{}{}
+ }
+ for _, v1 := range g1.Members {
+ if _, ok := memberSet[v1]; !ok {
return false
}
}
@@ -5698,16 +5696,14 @@
func isUserInfoEqual(u1, u2 madmin.UserInfo) bool {
if u1.PolicyName != u2.PolicyName ||
u1.Status != u2.Status ||
u1.SecretKey != u2.SecretKey {
return false
}
- for len(u1.MemberOf) != len(u2.MemberOf) {
+ if len(u1.MemberOf) != len(u2.MemberOf) {
return false
}
- for _, v1 := range u1.MemberOf {
- var found bool
- if slices.Contains(u2.MemberOf, v1) {
- found = true
- }
- if !found {
+ groupSet := make(map[string]struct{}, len(u2.MemberOf))
+ for _, g := range u2.MemberOf {
+ groupSet[g] = struct{}{}
+ }
+ for _, v1 := range u1.MemberOf {
+ if _, ok := groupSet[v1]; !ok {
return false
}
}