Skip to content

detect_stops: bound scan work — fix the 100k-point DoS - #65

Merged
rslayer merged 1 commit into
masterfrom
cockpit/detect-stops-scan-budget
Jul 21, 2026
Merged

detect_stops: bound scan work — fix the 100k-point DoS#65
rslayer merged 1 commit into
masterfrom
cockpit/detect-stops-scan-budget

Conversation

@rslayer

@rslayer rslayer commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Bound detect_stops scan work — the 100k-point DoS

The availability hole found while measuring #64: detect_stops re-grows a spatial cluster from each start sample when the previous one is rejected. A crafted single giant cluster (a very slow drift, or a same-timestamp mass) is never accepted, so the scan runs O(n·cluster_size)~70 s of CPU at the 100k-point cap, from one unauthenticated POST /v1/insights (the 30/min rate limit doesn't close it). Progressive-movement rejection (#63) widened it.

Two bounded, real-data-safe guards (ADR 0016):

  1. Zero-duration skip (exact): a cluster whose samples share one timestamp can't be a stop, so it's skipped wholesale rather than re-grown. A 100k same-timestamp body drops ~70 s → ~0.8 s. Never touches a real track.
  2. Absolute scan-work budget (12M units, ≈ 5 s), where work counts grown samples + the window size of every exact-radius recompute (the real time driver). compute_insights catches it and degrades gracefully — no stops (nor trips) + a quality note, but still returns distance, speed, bbox, hotspots. Absolute (not per-point) so it never false-positives a small dense track whose absolute time is fine.

Verified

input before after
100k slow-drift (attack) ~73 s 5.5 s (skipped + noted)
100k same-timestamp (attack) ~70 s 0.8 s
41k real drive+stop+drive 0.77 s, 1 stop (unaffected)
6k dense 0.62 s (unaffected)

Full suite: +4 DoS tests, zero new failures.

⚠️ Residual (flagged, not fixed)

~5 s is a bound, not a real O(n) fix — a provably-O(n) detect_stops needs an incrementally-maintained centroid spread (dynamic farthest-point) or a results-changing metric, out of proportion here. Follow-up options: the O(n) sliding-window rewrite, or lowering MAX_PUBLIC_POINTS (a product call) to shrink the absolute worst case. Noted in ADR 0016 + memory.

detect_stops re-grows a spatial cluster from each start sample when the previous
one is rejected (too short, or progressive movement). A crafted single giant
cluster (a slow drift, or a same-timestamp mass) is never accepted, so the scan
runs O(n*cluster_size) — ~70s of CPU at the 100k-point cap, from one
unauthenticated POST /v1/insights (an availability/DoS hole the 30/min rate
limit doesn't close). Progressive-movement rejection (ADR 0014) widened it.

Two bounded, real-data-safe guards (ADR 0016):
- Zero-duration skip (exact): a cluster whose samples share one timestamp can
  never be a stop, so it's skipped wholesale, not re-grown. 100k same-ts body:
  ~70s -> ~0.8s. Never affects a real track.
- Absolute scan-work budget (_MAX_STOP_SCAN_WORK=12M, ~5s), where work counts
  grown samples + the window size of every exact-radius recompute (the real
  time driver). compute_insights catches StopScanBudgetExceeded and degrades
  gracefully: no stops (nor trips) + a quality note, but still returns distance,
  speed, bbox, hotspots. Absolute (not per-point) so it never false-positives a
  small dense track whose absolute time is fine.

Verified: 100k slow-drift 73s -> 5.5s (skipped+noted), 100k same-ts -> 0.8s,
41k real drive+stop+drive 0.77s (1 stop), 6k dense 0.62s — both unaffected.
Full suite: +4 DoS tests, zero new failures.

Residual (flagged): ~5s is a bound, not a true O(n) fix. Follow-up = an O(n)
sliding-window detect_stops (hard) or lowering MAX_PUBLIC_POINTS (product call).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rslayer
rslayer merged commit fb2e87b into master Jul 21, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant