New UNDF assignments (693→720): elixir-0002 → UNDF-2026-000000698 (typespec used_type_pairs O(T²)) r-source-0002 → UNDF-2026-000000711 (.walkClassGraph match dedup O(S²)) ruby-0003 → UNDF-2026-000000712 (RubyGems dependent_gems O(N²×D)) victoria-metrics-0002 → UNDF-2026-000000717 (MetricName tag-filter O(T×I)) Total: 720 UNDF assigned
3.9 KiB
UNDF: UNDF-2026-000000709
otel-collector-0002: CWE-407 — O(N²) linear table scan in pprofile StringTable during profile batching
Severity: HIGH
Repository
github.com/open-telemetry/opentelemetry-collector Commit: HEAD (main branch)
Files
pdata/pprofile/string_table.go
pdata/pprofile/functions.go
pdata/pprofile/mappings.go
pdata/pprofile/locations.go
pdata/pprofile/stacks.go
Defective Lines
string_table.go:16 — SetString
func SetString(table pcommon.StringSlice, val string) (int32, error) {
for j, v := range table.All() { // O(N) linear scan over growing table
if v == val {
return int32(j), nil
}
}
table.Append(val)
return int32(table.Len() - 1), nil
}
functions.go:16 — SetFunction
func SetFunction(table FunctionSlice, fn Function) (int32, error) {
for j, m := range table.All() { // O(N) linear scan
if m.Equal(fn) {
return int32(j), nil
}
}
fn.CopyTo(table.AppendEmpty())
return int32(table.Len() - 1), nil
}
Same pattern in SetMapping, SetLocation, SetStack, SetAttribute, SetLink.
Call Chain — Hot Path
exporter/exporterhelper/xexporterhelper/profiles_batch.go:54
profilesRequest.mergeTo(dst, sz)
→ pprofile.Profiles.MergeTo(dest) // profiles_merge.go:16
→ Profiles.switchDictionary(src, dst) // profiles.go:37
→ for each Function: fn.switchDictionary(src, dst)
→ SetString(dst.StringTable(), fnName) // O(T) scan per call
→ SetString(dst.StringTable(), sysName)
→ SetString(dst.StringTable(), filename)
→ for each Mapping: mapping.switchDictionary(src, dst)
→ SetString(dst.StringTable(), filename)
→ for each Sample: sample.switchDictionary(src, dst)
→ SetStack(dst.StackTable(), stack) // O(S) scan
Complexity
For a profile with F functions (3 string refs each), M mappings (1 ref), S stacks, T unique strings in the destination table:
O((3F + M + ...) × T) for string lookups alone
where T grows as strings are added. For a profile with 1,000 functions and
500 unique strings this is ~1.5M string comparisons per MergeTo call.
Called from MergeSplit in the exporter batching hot path — invoked on every
export batch when the profiling pipeline is active (e.g., continuous profiling
with OpenTelemetry collector).
Fix
Replace the O(N) linear scan in each Set* function with a helper that builds
a string→index map once before the merge loop and reuses it for O(1) lookups:
// stringTableIndex builds a value→index map for O(1) duplicate detection.
func stringTableIndex(table pcommon.StringSlice) map[string]int32 {
idx := make(map[string]int32, table.Len())
for i, v := range table.All() {
idx[v] = int32(i)
}
return idx
}
// SetString uses the pre-built index for O(1) lookup instead of O(N) scan.
func SetStringWithIndex(table pcommon.StringSlice, idx map[string]int32, val string) (int32, error) {
if j, ok := idx[val]; ok {
return j, nil
}
if table.Len() >= math.MaxInt32 {
return 0, errTooManyStringTableEntries
}
table.Append(val)
newIdx := int32(table.Len() - 1)
idx[val] = newIdx
return newIdx, nil
}
The switchDictionary methods should accept the pre-built index maps and pass
them through the call chain rather than re-scanning the table on every call.
Alternatively, MergeTo can build all indexes once and pass them down.
Benchmark
Profile with F=1000 functions, M=500 mappings, T=800 unique strings:
- Before: ~1,900,000 string comparisons per merge
- After: ~2,300 map lookups per merge (hash table)
- Speedup: ~826x at these parameters
- Actual: O(N²) → O(N)
Unit Test
See defects/otel-collector/unit/OtelCollector0002Test.java
Related
- otel-collector-0001:
pcommon/map.goPutStr linear probe (same pattern, different package) - Affects: all profiling pipelines using continuous profiling receivers that batch exports