1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
#![cfg(feature = "gpu")]
//! LANE 1 (GPU CORRECTNESS), live GPU≡SIMD on a real multi-detector corpus,
//! pinning the region-presence GPU path in `engine/gpu_region_dispatch.rs`.
//!
//! Two contracts the validated trigger production must hold (both Law 10):
//!
//! 1. **Finding-set equality**: the GPU path's finding set is IDENTICAL to the
//! SIMD path's on a corpus that mixes real secrets, GPU over-fire bait (a
//! detector literal with NO valid body), and clean files. The wave-1 masking
//! dropped one finding (523 vs 524) by trusting the GPU exclusively; the
//! recall floor closes that, so neither backend may have a finding the other
//! lacks.
//!
//! 2. **No over-firing inflation**: the GPU path's user-visible findings come
//! ONLY from chunks where the detector truly matches. The over-fire-bait chunk
//! (`ghp_` literal, no 36-char body) must produce ZERO findings on BOTH
//! backends. Region presence is allowed to produce a candidate bit; the shared
//! phase-2 extractor must reject it.
//!
//! These run on a live adapter; gated by the explicit require-GPU runtime policy
//! to hard-fail in CI that mandates a GPU, else skipped (no silent CPU
//! masquerade (the GPU path is exercised explicitly via `ScanBackend::GpuWgpu`)).
//!
//! Run: cargo test -p keyhog-scanner --features gpu --test gpu_region_overfire_validation -- --nocapture
#[path = "support/mod.rs"]
mod support;
use keyhog_scanner::{CompiledScanner, ScanBackend};
use support::contracts::test_chunk;
use support::gpu_gate::require_gpu_or_panic;
use support::paths::detector_dir;
/// (credential, file_path, offset), the user-visible finding identity, matching
/// `gpu_parity.rs`. Detector id is intentionally excluded (a literal can attribute
/// to a different detector when prefixes overlap; the credential + location is the
/// product surface).
type FindingKey = (String, String, usize);
fn keys(results: &[Vec<keyhog_core::RawMatch>]) -> std::collections::BTreeSet<FindingKey> {
let mut set = std::collections::BTreeSet::new();
for chunk in results {
for m in chunk {
set.insert((
m.credential.as_ref().to_string(),
m.location
.file_path
.as_deref()
.map(str::to_string)
.unwrap_or_default(),
m.location.offset,
));
}
}
set
}
fn canonical(results: &[Vec<keyhog_core::RawMatch>]) -> Vec<keyhog_core::RawMatch> {
let mut findings = results.iter().flatten().cloned().collect::<Vec<_>>();
findings.sort();
findings
}
fn scan_gpu_without_degrade(
scanner: &CompiledScanner,
chunks: &[keyhog_core::Chunk],
) -> Vec<Vec<keyhog_core::RawMatch>> {
let before = scanner.runtime_status().gpu_degrade_count;
let results = scanner
.scan_chunks_with_backend(chunks, ScanBackend::GpuWgpu)
.expect("selected backend scan succeeds");
let after = scanner.runtime_status().gpu_degrade_count;
assert_eq!(
after, before,
"GPU parity evidence is invalid because the measured scan degraded to another backend"
);
results
}
fn scanner() -> CompiledScanner {
let detectors =
keyhog_core::load_detectors(&detector_dir()).expect("detectors directory must load");
CompiledScanner::compile(detectors).expect("scanner compile")
}
/// The core lane gate: GPU finding set ≡ SIMD finding set on a corpus that
/// deliberately includes GPU over-fire bait. Any divergence, a GPU under-fire
/// (the 523-vs-524 recall drop) OR a GPU-only finding (over-fire reaching
/// phase-2) (fails with the exact diff).
#[test]
fn gpu_equals_simd_on_overfire_bait_corpus() {
require_gpu_or_panic("gpu_equals_simd_on_overfire_bait_corpus");
let scanner = scanner();
let valid_ghp = "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"; // ghp_ + 36 checksum-valid body
assert_eq!(valid_ghp.len(), 40, "ghp_ token must be 40 chars");
let chunks = vec![
// Real GitHub PAT (a finding on BOTH backends).
test_chunk(
&format!("const TOKEN = \"{valid_ghp}\";"),
"fixtures/real_pat.rs",
),
// OVER-FIRE BAIT: the `ghp_` literal Hyperscan keys on, but the body is
// far too short for `ghp_[A-Za-z0-9]{36}`. The unanchored GPU DFA fires
// along the prefix; validation must drop it so it never reaches phase-2.
// ZERO findings expected on both backends.
test_chunk(
"note: the prefix ghp_ is mentioned but ghp_short is not a token",
"fixtures/overfire_bait.md",
),
// Real AWS key (exercises a second detector class (AKIA)).
test_chunk(
"aws_access_key_id = AKIAQYLPMN5HFIQR7XYA",
"fixtures/aws.ini",
),
// Clean file (no detector literal at all).
test_chunk("fn main() { println!(\"hello, world\"); }", "src/clean.rs"),
];
let simd_results = scanner
.scan_chunks_with_backend(&chunks, ScanBackend::SimdCpu)
.expect("selected backend scan succeeds");
let gpu_results = scan_gpu_without_degrade(&scanner, &chunks);
let simd = keys(&simd_results);
let gpu = keys(&gpu_results);
// The real PAT must be found by SIMD (sanity: the corpus has a finding).
assert!(
simd.iter().any(|(cred, _, _)| cred == valid_ghp),
"SIMD must find the valid ghp_ PAT; got {simd:?}"
);
// The over-fire bait must NOT appear as a finding on either backend.
assert!(
!simd
.iter()
.any(|(_, path, _)| path == "fixtures/overfire_bait.md"),
"SIMD must not surface a finding in the over-fire bait file: {simd:?}"
);
assert!(
!gpu.iter()
.any(|(_, path, _)| path == "fixtures/overfire_bait.md"),
"GPU must not surface a finding in the over-fire bait file (validation \
dropped the over-fire): {gpu:?}"
);
if simd != gpu {
let only_simd: Vec<_> = simd.difference(&gpu).collect();
let only_gpu: Vec<_> = gpu.difference(&simd).collect();
panic!(
"GPU≢SIMD on the over-fire-bait corpus.\n SIMD findings: {}\n GPU findings: {}\n \
only in SIMD (GPU under-fire / recall drop) ({}): {:?}\n \
only in GPU (over-fire reached phase-2) ({}): {:?}",
simd.len(),
gpu.len(),
only_simd.len(),
only_simd,
only_gpu.len(),
only_gpu,
);
}
assert_eq!(
canonical(&gpu_results),
canonical(&simd_results),
"GPU and SIMD must agree on every RawMatch field and multiplicity"
);
}
/// Boundary-straddled secret parity through the GPU path with the over-fire bait
/// present, proving the validated trigger production does not break cross-chunk
/// reassembly (the boundary scan runs AFTER phase-2 on the GPU path too).
#[test]
fn gpu_equals_simd_with_repeated_real_secrets() {
require_gpu_or_panic("gpu_equals_simd_with_repeated_real_secrets");
let scanner = scanner();
// Several real PATs across many chunks so the GPU produces MANY raw firings
// (the unanchored DFA re-accepts along each 40-char body) that must dedup to
// exactly the per-chunk finding set the SIMD path produces.
let toks = [
"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",
"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",
"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",
];
let mut chunks = Vec::new();
for (i, t) in toks.iter().enumerate() {
assert_eq!(t.len(), 40);
chunks.push(test_chunk(
&format!("line0\nKEY=\"{t}\"\nline2"),
&format!("fixtures/pat_{i}.env"),
));
// Interleave over-fire bait so the GPU fires the `ghp_` prefix with no body.
chunks.push(test_chunk(
"comment about ghp_ tokens without any real body here",
&format!("fixtures/bait_{i}.md"),
));
}
let simd_results = scanner
.scan_chunks_with_backend(&chunks, ScanBackend::SimdCpu)
.expect("selected backend scan succeeds");
let gpu_results = scan_gpu_without_degrade(&scanner, &chunks);
let simd = keys(&simd_results);
let gpu = keys(&gpu_results);
let simd_tokens: std::collections::BTreeSet<&str> = simd
.iter()
.map(|(credential, _, _)| credential.as_str())
.filter(|credential| toks.contains(credential))
.collect();
assert_eq!(
simd_tokens.len(),
toks.len(),
"SIMD must find all {} distinct real PATs; got {simd:?}",
toks.len()
);
assert_eq!(
simd,
gpu,
"GPU≢SIMD on the repeated-secret + bait corpus.\n only SIMD: {:?}\n only GPU: {:?}",
simd.difference(&gpu).collect::<Vec<_>>(),
gpu.difference(&simd).collect::<Vec<_>>(),
);
assert_eq!(
canonical(&gpu_results),
canonical(&simd_results),
"repeated-secret parity must include every RawMatch field and multiplicity"
);
}
/// Zero-width interior-evasion parity, the exact recall regression the
/// validated trigger production must NOT introduce. A `ghp_` PAT split by a
/// zero-width space (`\u{200B}`) does NOT match the detector regex on RAW bytes
/// but DOES after `prepare_chunk`'s interior-control strip. The SIMD path fires
/// the `ghp_` literal on raw and finds it on the stripped text. The GPU
/// validation oracle therefore MUST run on the PREPROCESSED text (not raw bytes),
/// or it would drop the bit and silently lose the finding vs SIMD (Law 10).
///
/// This pins that the oracle domain is preprocessed: SIMD and GPU must agree, and
/// both must surface the de-obfuscated secret.
#[test]
fn gpu_equals_simd_on_zero_width_obfuscated_secret() {
require_gpu_or_panic("gpu_equals_simd_on_zero_width_obfuscated_secret");
let scanner = scanner();
// ghp_ + 36 body, with a zero-width space inserted INSIDE the body. The raw
// bytes do not contain a 40-char `ghp_[A-Za-z0-9]{36}` run; the strip removes
// the ZWSP and reveals it.
let body = "1234567890ABCDEFghijklmnopqrst3yckgQ"; // 36 chars, checksum-valid
assert_eq!(body.len(), 36);
let (head, tail) = body.split_at(10);
let obfuscated = format!("ghp_{head}\u{200B}{tail}"); // ZWSP at offset 14
let clean = format!("ghp_{body}");
let chunks = vec![test_chunk(
&format!("token = \"{obfuscated}\""),
"fixtures/zw_obfuscated.rs",
)];
let simd_results = scanner
.scan_chunks_with_backend(&chunks, ScanBackend::SimdCpu)
.expect("selected backend scan succeeds");
let gpu_results = scan_gpu_without_degrade(&scanner, &chunks);
let simd = keys(&simd_results);
let gpu = keys(&gpu_results);
// SIMD must de-obfuscate and surface the cleaned token (the engine's unicode
// hardening contract (see the adversarial unicode_normalization suite)).
assert!(
simd.iter().any(|(cred, _, _)| cred == &clean),
"SIMD must surface the de-obfuscated ghp_ token {clean:?}; got {simd:?}"
);
// The GPU path, validating on the PREPROCESSED text, must agree exactly, no
// silent recall loss from validating on raw bytes.
assert_eq!(
simd,
gpu,
"GPU≢SIMD on the zero-width-obfuscated secret, the validation oracle must \
run on preprocessed text, not raw bytes.\n only SIMD: {:?}\n only GPU: {:?}",
simd.difference(&gpu).collect::<Vec<_>>(),
gpu.difference(&simd).collect::<Vec<_>>(),
);
assert_eq!(
canonical(&gpu_results),
canonical(&simd_results),
"unicode-normalized parity must include every RawMatch field and multiplicity"
);
}