Skip to content

feat(detection): per-request scan time budget (detection_max_scan_seconds) - #139

Merged
rennf93 merged 2 commits into
masterfrom
feat/scan-time-budget
Oct 15, 2026
Merged

rennf93 merged 2 commits into
masterfrom
feat/scan-time-budget

Conversation

@rennf93

@rennf93 rennf93 commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Closes #138

I am porting the per-request scan time budget from guard-core (Guard-Core/guard-core #167) to the Rust engine. The TypeScript port is guard-core-ts #132 and the JVM port is guard-core-jvm #53.

  • SecurityConfig::detection_max_scan_seconds: default 10.0, valid 0.1 to 60, validated with the other bounds.
  • A per-request cap on the summed regex scan CPU time, across every value and every view pass, checked between patterns. It is thread CPU time, not wall time, as in the reference (wall time exhausted the budget falsely on loaded hosts).
  • Once spent, the remaining patterns and values are skipped and the scan records the reference threat: a weighted pattern_timeout named scan_time_budget_exhausted (empty match, position 0, category custom, weight 1.0), exposed as the new Threat::PatternTimeout. scan_request shares one budget across all values of the request and reports a threat value with the reason Pattern exceeded scan time budget: 'scan_time_budget_exhausted'. The budget threat is an ordinary weighted threat, so is_threat follows the normal detection_threat_score_threshold rule exactly like the reference (a threshold above 1.0 leaves a budget-only value clean), and the enabled-category filter does not drop it. When a value already matched an enabled category the reason keeps it and appends the budget text, the way the reference _build_threats_message does.
  • The reference also yields to its event loop every couple of milliseconds. That half does not apply here: the scan is synchronous on the calling task, so I added no async yielding.

Where the cap lives

DetectConfig is unchanged. The cap rides on DetectionExclusionConfig::max_scan_seconds (Option<f64>, None is the default of 10.0) and ResolvedExclusions::max_scan_seconds, the engine's port of the reference detection_config.py resolvers. A new DetectConfig field would break every adapter's DetectConfig literal on master and turn the ecosystem gate red, whereas the adapters already build DetectionExclusionConfig with ..Default::default(), as the semantic header opt-out did in #137. detect on its own gives each call a fresh default budget, and detect_with_budget takes a caller-owned ScanTimeBudget.

The CPU clock

The engine crate has unsafe_code = "forbid" (crates/guard-core-engine/Cargo.toml), so a direct libc::clock_gettime call is not possible, although libc is already in the engine's tree (through cpufeatures and getrandom). I used the safe rustix::time::clock_gettime(ClockId::ThreadCPUTime) wrapper, declared only for Linux, Android and macOS. rustix was in Cargo.lock already (through tempfile, for the rocket example) but not in the engine's tree, so this adds one direct dependency for those targets, and the lock gains one line. Other targets fall back to wall time, which can only exhaust the budget earlier, never later. cargo deny check and cargo audit pass.

Compatibility

  • Threat gains the PatternTimeout variant, so exhaustive matches need an arm (none of the four adapters matches on Threat).
  • DetectionExclusionConfig and ResolvedExclusions no longer derive Eq (they carry an f64), and ResolvedExclusions gains a public field.
  • A NaN cap counts as exhausted (fail closed).
  • The conformance corpus is untouched and unaffected: the default budget never trips on it.

Tests

The new tests failed to compile on master (no variant ... named PatternTimeout, cannot find function detect_with_budget, no field detection_max_scan_seconds on type SecurityConfig) and pass now. They cover the field default and the 0.1 to 60 range, the exact threat shape, a budget shared across values, an unchanged result within budget, the reason naming the budget, the budget threat reported under a category filter and not blocking above a 1.0 threshold, and the real thread clock advancing.

I ran everything ci.yml runs: fmt, both clippy variants with -D warnings, cargo test --workspace, ledger, conformance and pipeline conformance gates, cargo doc with -D warnings, MSRV 1.92 check and test, cargo deny, cargo audit, and the 100% line coverage gate (49232/49232). The adapters wire the field through in their own PRs, which need this one merged first.

…onds)

Port of the guard-core reference budget (guard-core #167). The summed regex scan CPU time of one request is capped, checked between patterns, and a spent budget records a weighted pattern_timeout threat named scan_time_budget_exhausted. The thread CPU clock goes through the safe rustix wrapper because the engine forbids unsafe.
@rennf93 rennf93 added enhancement Improvement to an existing feature area: engine Touches crates/guard-core-engine/ (compiler, preprocessor, semantic, detect, patterns) labels Oct 15, 2026
@github-actions github-actions Bot added documentation Docs site, README, CHANGELOG, license files tests Test suite changes dependencies Cargo.toml or Cargo.lock area: facade Touches crates/guard-core-rs/ (published facade crate) area: python-bindings Touches crates/guard-core-python/ (PyO3 detection bindings) area: conformance Touches the spec 4.0.2 corpus, ledger, or xfail baseline labels Oct 15, 2026
@rennf93 rennf93 added the breaking-change PR contains a backward-incompatible change label Oct 15, 2026
…old rule

The budget threat is an ordinary weighted regex-side threat in the reference, so scan_request no longer blocks on it regardless of detection_threat_score_threshold. is_threat follows the threshold rule, and a threat value is reported with the budget reason without the category filter dropping it.
@rennf93
rennf93 merged commit 45e506b into master Oct 15, 2026
20 checks passed
@rennf93
rennf93 deleted the feat/scan-time-budget branch October 15, 2026 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: conformance Touches the spec 4.0.2 corpus, ledger, or xfail baseline area: engine Touches crates/guard-core-engine/ (compiler, preprocessor, semantic, detect, patterns) area: facade Touches crates/guard-core-rs/ (published facade crate) area: python-bindings Touches crates/guard-core-python/ (PyO3 detection bindings) breaking-change PR contains a backward-incompatible change dependencies Cargo.toml or Cargo.lock documentation Docs site, README, CHANGELOG, license files enhancement Improvement to an existing feature tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(detection): per-request scan time budget (detection_max_scan_seconds)

1 participant