Finding
The convergence grid retains and materializes full per-cell hit history even though the detection path only ever needs a bounded per-cell distinct-domain count. This surfaces as two reinforcing failures. (1) ConvergenceGrid::ingest appends a DomainHit to a per-cell Vec unconditionally — no per-cell cap, no per-domain dedup — so only time-based eviction reclaims entries and within the eviction window a single attacker-chosen coordinate accumulates hits at the input rate. (2) SemainoPipeline::handle_aggregated runs ConvergenceGrid::detect once per notable aggregated signal; detect iterates every grid cell and clones every in-window DomainHit into the returned Convergence values, yet the only field any consumer reads is domain_count (itself capped at 7 distinct domains).
Evidence
crates/semaino/src/pipeline.rs:219 .detect(self.min_convergence_domains, self.time_window, now); runs for each notable aggregated signal (handle_aggregated is invoked at pipeline.rs:173).
crates/semaino/src/convergence.rs:117 self.cells.entry(cell).or_default().push(DomainHit { appends every located signal; eviction at convergence.rs:170 only drops hits older than the window, so within the window a cell's Vec grows with input rate.
crates/semaino/src/convergence.rs:158 hits: window_hits.iter().map(|h| (*h).clone()).collect(), clones all windowed hits across all cells on every detect.
domain_count is bounded: crates/semaino/src/convergence.rs:194 kind_discriminant yields at most 7 distinct domains, so storing or cloning more than one hit per domain per cell is pure overhead.
Downstream only domain_count is read — alert.rs:227 c.domain_count >= 3, alert.rs:300 conv.domain_count; Convergence.hits has no non-test consumer.
Why this matters
The documented trust model lets an OTA adversary stream many frames at one spoofed coordinate and treats OTA-driven resource exhaustion as directly harmful. The matched cell's Vec then holds attacker-rate × window DomainHits (each cloning a SignalKind) with no hard ceiling and no backpressure beyond channel buffering — attacker-controlled memory growth inside the correlation engine. That same redundant storage amplifies detect: per-signal work and allocation scale with the total in-window hit count across the entire grid rather than the one relevant cell, so under a flood that makes many signals notable this is O(N^2) CPU plus large transient allocations of data that is immediately discarded — all on the single-threaded main select! loop, degrading alert latency and providing an OTA-driven CPU/memory DoS amplifier.
Desired correction
Represent per-cell state as a bounded distinct-domain structure — at most one retained hit per of the ≤7 domain discriminants (a 7-entry map) or a bounded ring — so per-cell memory is independent of input rate; scope detect to the cell the triggering signal landed in (or maintain incremental per-cell distinct-domain counts) rather than scanning the whole grid; and stop materializing the hits clone no consumer reads. Done when: a sustained flood at a single coordinate cannot grow a cell's stored hits beyond a fixed bound, and per-aggregated-signal convergence work is bounded by the relevant cell's contents with no Vec<DomainHit> clone produced for cells or fields the alert path never consumes.
Finding
The convergence grid retains and materializes full per-cell hit history even though the detection path only ever needs a bounded per-cell distinct-domain count. This surfaces as two reinforcing failures. (1)
ConvergenceGrid::ingestappends aDomainHitto a per-cellVecunconditionally — no per-cell cap, no per-domain dedup — so only time-based eviction reclaims entries and within the eviction window a single attacker-chosen coordinate accumulates hits at the input rate. (2)SemainoPipeline::handle_aggregatedrunsConvergenceGrid::detectonce per notable aggregated signal;detectiterates every grid cell and clones every in-windowDomainHitinto the returnedConvergencevalues, yet the only field any consumer reads isdomain_count(itself capped at 7 distinct domains).Evidence
crates/semaino/src/pipeline.rs:219.detect(self.min_convergence_domains, self.time_window, now);runs for each notable aggregated signal (handle_aggregatedis invoked at pipeline.rs:173).crates/semaino/src/convergence.rs:117self.cells.entry(cell).or_default().push(DomainHit {appends every located signal; eviction at convergence.rs:170 only drops hits older than the window, so within the window a cell'sVecgrows with input rate.crates/semaino/src/convergence.rs:158hits: window_hits.iter().map(|h| (*h).clone()).collect(),clones all windowed hits across all cells on everydetect.domain_countis bounded:crates/semaino/src/convergence.rs:194kind_discriminantyields at most 7 distinct domains, so storing or cloning more than one hit per domain per cell is pure overhead.Downstream only
domain_countis read — alert.rs:227c.domain_count >= 3, alert.rs:300conv.domain_count;Convergence.hitshas no non-test consumer.Why this matters
The documented trust model lets an OTA adversary stream many frames at one spoofed coordinate and treats OTA-driven resource exhaustion as directly harmful. The matched cell's
Vecthen holds attacker-rate × windowDomainHits (each cloning aSignalKind) with no hard ceiling and no backpressure beyond channel buffering — attacker-controlled memory growth inside the correlation engine. That same redundant storage amplifiesdetect: per-signal work and allocation scale with the total in-window hit count across the entire grid rather than the one relevant cell, so under a flood that makes many signals notable this is O(N^2) CPU plus large transient allocations of data that is immediately discarded — all on the single-threaded mainselect!loop, degrading alert latency and providing an OTA-driven CPU/memory DoS amplifier.Desired correction
Represent per-cell state as a bounded distinct-domain structure — at most one retained hit per of the ≤7 domain discriminants (a 7-entry map) or a bounded ring — so per-cell memory is independent of input rate; scope
detectto the cell the triggering signal landed in (or maintain incremental per-cell distinct-domain counts) rather than scanning the whole grid; and stop materializing thehitsclone no consumer reads. Done when: a sustained flood at a single coordinate cannot grow a cell's stored hits beyond a fixed bound, and per-aggregated-signal convergence work is bounded by the relevant cell's contents with noVec<DomainHit>clone produced for cells or fields the alert path never consumes.