keyhog_scanner/compiled_scanner/
compile.rs1#[cfg(feature = "simdsieve")]
2use super::compile_helpers::build_hot_pattern_slots;
3#[cfg(all(target_os = "linux", feature = "gpu"))]
4use super::compile_helpers::surface_cuda_acquisition_failure;
5use super::*;
6
7impl CompiledScanner {
8 pub fn compile(detectors: Vec<DetectorSpec>) -> Result<Self> {
11 Self::compile_with_gpu_policy(detectors, GpuInitPolicy::FromRuntimePolicy)
12 }
13
14 pub fn compile_with_gpu_policy(
17 detectors: Vec<DetectorSpec>,
18 gpu_policy: GpuInitPolicy,
19 ) -> Result<Self> {
20 Self::compile_with_gpu_policy_and_tuning(
21 detectors,
22 gpu_policy,
23 &ScannerTuningConfig::default(),
24 )
25 }
26
27 pub fn compile_with_gpu_policy_and_tuning(
30 detectors: Vec<DetectorSpec>,
31 gpu_policy: GpuInitPolicy,
32 tuning_config: &ScannerTuningConfig,
33 ) -> Result<Self> {
34 super::validation::validate_detector_corpus(&detectors)
35 .map_err(crate::error::ScanError::Config)?;
36 crate::entropy::policy::validate_feature_compatibility(&detectors)
37 .map_err(crate::error::ScanError::Config)?;
38 let decoder_plan = Arc::new(crate::decode::CompiledDecoderPlan::snapshot().map_err(
39 |error| crate::error::ScanError::Config(format!("invalid decoder registry: {error}")),
40 )?);
41 #[cfg(not(feature = "simd"))]
43 let _tuning_config = tuning_config;
44 let state = build_compile_state(&detectors)?;
45 let static_intern_strings: Vec<&str> = detectors
49 .iter()
50 .flat_map(|detector| {
51 [
52 detector.id.as_str(),
53 detector.name.as_str(),
54 detector.service.as_str(),
55 ]
56 .into_iter()
57 .chain(
58 detector
59 .entropy_fallback
60 .as_ref()
61 .into_iter()
62 .flat_map(|metadata| {
63 [
64 metadata.id.as_str(),
65 metadata.name.as_str(),
66 metadata.service.as_str(),
67 ]
68 }),
69 )
70 })
71 .collect();
72 let static_intern = Arc::new(crate::static_intern::StaticInterner::from_detector_strings(
73 static_intern_strings,
74 ));
75 let detector_digest = super::detector_digest::from_execution_plan(
76 keyhog_core::compute_spec_hash(&detectors),
77 decoder_plan.identity(),
78 );
79 let detector_plans =
80 crate::detector_plan::CompiledDetectorPlans::compile_with_decoder_plan(
81 &detectors,
82 static_intern.as_ref(),
83 state.companions,
84 decoder_plan,
85 )
86 .map_err(crate::error::ScanError::Config)?;
87 validate_compiled_pattern_detector_indices(
88 &state.ac_map,
89 &state.phase2_patterns,
90 detectors.len(),
91 )?;
92 let ac = build_ac_pattern_set(&state.ac_literals)?;
93 let gpu_disabled = match gpu_policy {
106 GpuInitPolicy::FromRuntimePolicy => crate::gpu::gpu_disabled_by_policy(),
107 GpuInitPolicy::ForceEnabled => false,
108 GpuInitPolicy::ForceDisabled => true,
109 };
110 if gpu_disabled {
111 let disabled_by_policy = matches!(gpu_policy, GpuInitPolicy::ForceDisabled);
112 if disabled_by_policy {
113 tracing::info!(
114 target: "keyhog::routing",
115 "GPU init bypassed by caller policy; scanner will use CPU/SIMD paths"
116 );
117 } else {
118 tracing::info!(
119 target: "keyhog::routing",
120 "GPU init bypassed by resolved scanner policy; routing every chunk through the CPU/SIMD path"
121 );
122 }
123 }
124 #[cfg(feature = "gpu")]
125 let (gpu_backends, gpu_acquisition_failures) = if !gpu_disabled {
126 let mut peers = GpuBackendPeers::default();
127 let mut failures = Vec::new();
128 {
129 #[cfg(target_os = "linux")]
130 {
131 match super::types::probe_cuda_peer() {
132 Ok(caps) => {
133 peers.cuda_available = true;
134 peers.cuda_device_identity = Some(format!(
135 "{}:ordinal={}:cc={}.{}:vram={}",
136 caps.name,
137 caps.ordinal,
138 caps.compute_capability.0,
139 caps.compute_capability.1,
140 caps.total_memory
141 ));
142 match linux_cuda_runtime_identity() {
143 Ok(identity) => peers.cuda_runtime_identity = Some(identity),
144 Err(diagnostic) => {
145 tracing::warn!(
146 target: "keyhog::routing",
147 %diagnostic,
148 "CUDA peer acquired without reproducible runtime identity"
149 );
150 }
151 }
152 tracing::debug!(target: "keyhog::routing", "CUDA peer identity probed");
153 }
154 Err(error) => {
155 surface_cuda_acquisition_failure(&error);
156 failures.push(GpuBackendAcquisitionFailure {
157 backend: "cuda",
158 diagnostic: error.to_string(),
159 });
160 }
161 }
162 }
163 }
164 if let Some(probe) = crate::gpu::gpu_adapter_probe() {
165 peers.wgpu_available = true;
166 peers.wgpu_device_identity = Some(probe.device_identity.clone());
167 peers.wgpu_runtime_identity = Some(probe.runtime_identity.clone());
168 peers.wgpu_is_software = probe.is_software;
169 tracing::debug!(target: "keyhog::routing", "WGPU peer identity probed");
170 } else {
171 failures.push(GpuBackendAcquisitionFailure {
172 backend: "wgpu",
173 diagnostic: "WGPU adapter census found no adapters".to_string(),
174 });
175 }
176 (peers, failures)
177 } else {
178 (GpuBackendPeers::default(), Vec::new())
179 };
180
181 #[cfg(not(feature = "gpu"))]
187 let (gpu_backends, gpu_acquisition_failures) = {
188 let _ = gpu_disabled; (GpuBackendPeers::default(), Vec::new())
190 };
191 let prefix_propagation = CsrU32::from(build_prefix_propagation(&state.ac_literals));
192 let same_prefix_patterns = CsrU32::from(build_same_prefix_patterns(&state.ac_literals));
193
194 #[cfg(feature = "simd")]
198 let simd_compile_plan =
199 build_simd_compile_plan(&state.ac_map, &state.ac_literals, tuning_config);
200 #[cfg(feature = "simd")]
201 let simd_candidate_available = simd_compile_plan.is_some();
202
203 let (phase2_keyword_ac, phase2_keyword_to_patterns, phase2_keywords) =
204 build_phase2_keyword_ac(&state.phase2_patterns);
205 let phase2_keyword_count = phase2_keywords.len();
206 let phase2_keyword_to_patterns = CsrU32::from(phase2_keyword_to_patterns);
207 let phase2_always_active_indices = phase2_always_active_indices(&state.phase2_patterns);
210
211 let (phase2_anchor_index, ((suffix_gate_ac, ac_suffix_gate), confirmed_anchor_index)) =
226 rayon::join(
227 || Phase2AnchorIndex::build(&state.phase2_patterns, &phase2_always_active_indices),
228 || {
229 rayon::join(
230 || build_confirmed_suffix_gate(&state.ac_map),
231 || ConfirmedAnchorIndex::build(&state.ac_map),
232 )
233 },
234 );
235 let phase2_always_anchor_literal_count = phase2_anchor_index
236 .as_ref()
237 .map_or(0, |index| index.always_anchor_literals().len());
238 #[cfg(feature = "gpu")]
239 let confirmed_anchor_literals = confirmed_anchor_index
240 .as_ref()
241 .map_or(&[] as &[String], |index| index.anchor_literals());
242 #[cfg(feature = "gpu")]
243 let confirmed_anchor_literal_count = confirmed_anchor_literals.len();
244 #[cfg(feature = "gpu")]
245 let generic_keyword_literals = detector_plans
246 .generic_assignment()
247 .map(|plan| plan.stem_literals().map(str::to_owned).collect::<Vec<_>>())
248 .unwrap_or_default();
250 #[cfg(feature = "gpu")]
251 let generic_keyword_literal_count = generic_keyword_literals.len();
252 let gated = ac_suffix_gate.iter().filter(|g| !g.is_empty()).count();
253 #[cfg(feature = "gpu")]
254 let gpu_literals = if gpu_backends.availability().any() {
255 let phase2_always_anchor_literals = phase2_anchor_index
256 .as_ref()
257 .map_or(&[] as &[String], |index| index.always_anchor_literals());
258 build_gpu_literals(
259 &state.ac_literals,
260 &phase2_keywords,
261 phase2_always_anchor_literals,
262 confirmed_anchor_literals,
263 &generic_keyword_literals,
264 )
265 } else {
266 None
267 };
268 #[cfg(not(feature = "gpu"))]
269 let gpu_literals: Option<Arc<Vec<Vec<u8>>>> = None;
270 #[cfg(feature = "gpu")]
271 let gpu_max_literal_len = gpu_literals.as_ref().map_or(0, |literals| {
272 literals
273 .iter()
274 .fold(0, |longest, literal| longest.max(literal.len()))
275 });
276
277 let phase2_always_active_prefilter = phase2::Phase2AlwaysActivePrefilter::build(
282 &state.phase2_patterns,
283 &phase2_always_active_indices,
284 phase2_anchor_index.as_ref(),
285 );
286 tracing::debug!(
287 eligible = phase2_anchor_index
288 .as_ref()
289 .map_or(0, |i| i.eligible_count()),
290 total = state.phase2_patterns.len(),
291 always_active = phase2_always_active_indices.len(),
292 "phase-2 prefilter built with homoglyph ASCII-folded fast path"
293 );
294
295 tracing::debug!(
296 gated,
297 anchored = confirmed_anchor_index
298 .as_ref()
299 .map_or(0, |index| index.eligible_count()),
300 total = state.ac_map.len(),
301 "confirmed suffix/anchor gates built"
302 );
303
304 log_quality_warnings(&state.quality_warnings);
305
306 let mut alphabet_targets = state.ac_literals.clone();
307 let extra_keyword_count: usize = state
313 .phase2_patterns
314 .iter()
315 .map(|(_, keywords)| keywords.len())
316 .sum();
317 alphabet_targets.reserve(extra_keyword_count);
318 for (_, keywords) in &state.phase2_patterns {
319 alphabet_targets.extend(keywords.iter().cloned());
320 }
321 let alphabet_screen = if alphabet_targets.is_empty() {
322 None
323 } else {
324 Some(crate::alphabet_filter::AlphabetScreen::new(
325 &alphabet_targets,
326 ))
327 };
328
329 let bigram_bloom =
330 crate::bigram_bloom::BigramBloom::from_literal_prefixes(&alphabet_targets);
331 tracing::debug!(
332 popcount = bigram_bloom.popcount(),
333 "bigram bloom built (65536 slots / 8 KB direct table, lower popcount = stronger filter)"
334 );
335
336 let missing_weak_anchor_floors = detectors
340 .iter()
341 .enumerate()
342 .filter_map(|(index, detector)| {
343 let has_weak_pattern = match detector_plans.get(index).weak_anchor_base {
344 crate::suppression::WeakAnchorBase::Always => true,
345 crate::suppression::WeakAnchorBase::PerPattern => {
346 detector.patterns.iter().any(|pattern| pattern.weak_anchor)
347 }
348 crate::suppression::WeakAnchorBase::Never => false,
349 };
350 (has_weak_pattern && detector_plans.get(index).entropy_floor.is_none())
351 .then_some(detector.id.as_str())
352 })
353 .collect::<Vec<_>>();
354 if !missing_weak_anchor_floors.is_empty() {
355 return Err(crate::error::ScanError::Config(format!(
356 "weak-anchor detectors omit detector-local entropy_high/entropy_floor policy: {}",
357 missing_weak_anchor_floors.join(", ")
358 )));
359 }
360 #[cfg(feature = "simdsieve")]
364 let hot_pattern_slots = build_hot_pattern_slots(&detectors, &state.ac_map)?;
365 #[cfg(feature = "simdsieve")]
366 let hot_confirmed_by_pattern = {
367 let mut hot = vec![false; state.ac_map.len()];
368 for slot in &hot_pattern_slots {
369 hot[slot.ac_map_index] = true;
370 }
371 hot
372 };
373 #[cfg(not(feature = "simdsieve"))]
374 let hot_confirmed_by_pattern = vec![false; state.ac_map.len()];
375
376 let pattern_boundary_context = derive_pattern_boundary_context(
377 state
378 .ac_map
379 .iter()
380 .chain(state.phase2_patterns.iter().map(|(pattern, _)| pattern)),
381 );
382 #[cfg(feature = "gpu")]
383 let ac_match_upper_bounds: Vec<Option<usize>> = state
384 .ac_map
385 .iter()
386 .map(|pattern| regex_match_byte_upper_bound(pattern.regex.as_str()))
387 .collect();
388
389 let mut structural_confirmed_patterns = vec![Vec::new(); detectors.len()];
390 for (pattern_index, pattern) in state.ac_map.iter().enumerate() {
391 if pattern.structural_password_slot {
392 structural_confirmed_patterns[pattern.detector_index].push(pattern_index);
393 }
394 }
395 let mut structural_phase2_patterns = vec![Vec::new(); detectors.len()];
396 for (pattern_index, (pattern, _)) in state.phase2_patterns.iter().enumerate() {
397 if pattern.structural_password_slot {
398 structural_phase2_patterns[pattern.detector_index].push(pattern_index);
399 }
400 }
401 let scanner = Self {
402 detector_digest,
403 ac,
404 gpu_backends,
405 gpu_acquisition_failures,
406 gpu_literals,
407 #[cfg(feature = "gpu")]
408 gpu_max_literal_len,
409 gpu_matcher: OnceLock::new(),
410 #[cfg(feature = "gpu")]
411 gpu_resident_literal_cuda: std::sync::Mutex::new(GpuResidentLiteralSlot::Empty),
412 #[cfg(feature = "gpu")]
413 gpu_resident_literal_wgpu: std::sync::Mutex::new(GpuResidentLiteralSlot::Empty),
414 gpu_last_degrade_reason: std::sync::Mutex::new(None),
415 gpu_degrade_count: std::sync::atomic::AtomicU64::new(0),
416 autoroute_gpu_shared_cold_ns: std::sync::atomic::AtomicU64::new(0),
417 static_intern,
418 detector_plans,
419 assignment_keyword_matcher: std::sync::Mutex::new(
420 crate::assignment_keyword_matcher::AssignmentKeywordMatcherCache::default(),
421 ),
422 #[cfg(feature = "gpu")]
423 ac_match_upper_bounds,
424 suffix_gate_ac,
425 ac_suffix_gate,
426 hot_confirmed_by_pattern,
427 confirmed_anchor_index,
428 ac_map: state.ac_map,
429 pattern_boundary_context,
430 prefix_propagation,
431 phase2_patterns: state.phase2_patterns,
432 structural_confirmed_patterns,
433 structural_phase2_patterns,
434 same_prefix_patterns,
435 phase2_keyword_ac,
436 phase2_keyword_to_patterns,
437 phase2_keyword_count,
438 phase2_always_anchor_literal_count,
439 #[cfg(feature = "gpu")]
440 confirmed_anchor_literal_count,
441 #[cfg(feature = "gpu")]
442 generic_keyword_literal_count,
443 phase2_always_active_indices,
444 phase2_always_active_prefilter,
445 phase2_anchor_index,
446 #[cfg(feature = "gpu")]
447 phase2_gpu_dfa: Phase2GpuDfaCatalogCache::default(),
448 tuning: phase2::ScannerTuning::from_defaults(),
449 #[cfg(feature = "simd")]
450 simd_candidate_available,
451 #[cfg(feature = "simd")]
452 simd_compile_plan: std::sync::Mutex::new(simd_compile_plan),
453 #[cfg(feature = "simd")]
454 simd_prefilter: std::sync::OnceLock::new(),
455 #[cfg(feature = "simd")]
456 simd_initialization_ns: std::sync::atomic::AtomicU64::new(0),
457 #[cfg(feature = "simdsieve")]
458 hot_pattern_slots,
459 config: ScannerConfig::default(),
460 alphabet_screen,
461 bigram_bloom,
462 fragment_cache: crate::fragment_cache::FragmentCache::new(1000),
463 };
464
465 Ok(scanner)
466 }
467
468 pub fn with_config(mut self, config: ScannerConfig) -> Self {
470 profile::set_profile_enabled(config.profile);
471 profile::set_perf_trace_enabled(config.perf_trace);
472 self.config = config;
473 self
474 }
475
476 pub fn with_tuning_config(self, config: ScannerTuningConfig) -> Self {
478 self.tuning.apply_config(&config);
479 self
480 }
481}
482
483#[cfg(all(target_os = "linux", feature = "gpu"))]
484fn linux_cuda_runtime_identity() -> std::result::Result<String, String> {
485 let version = std::fs::read_to_string("/proc/driver/nvidia/version")
486 .map_err(|error| format!("cannot read /proc/driver/nvidia/version: {error}"))?;
487 let version = version.split_whitespace().collect::<Vec<_>>().join(" ");
488 if version.is_empty() {
489 Err("/proc/driver/nvidia/version contains no runtime identity".to_owned())
490 } else {
491 Ok(format!("nvidia-kernel:{version}"))
492 }
493}