Skip to content

Commit 7c48706

Browse files
committed
Add fixture specialization detectors
1 parent babd880 commit 7c48706

6 files changed

Lines changed: 486 additions & 35 deletions

File tree

.github/workflows/blue-pr-prechecks.yml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
name: blue-pr-prechecks
22

33
on:
4-
pull_request:
5-
types:
6-
- opened
7-
- synchronize
8-
- reopened
9-
- ready_for_review
10-
- edited
4+
workflow_dispatch:
115

126
permissions:
137
contents: read
148
pull-requests: read
159

1610
concurrency:
17-
group: blue-pr-prechecks-${{ github.event.pull_request.number }}
11+
group: blue-pr-prechecks-${{ github.event.pull_request.number || github.run_id }}
1812
cancel-in-progress: true
1913

2014
jobs:
2115
prechecks:
16+
# Temporarily disabled while blue submissions are inactive.
17+
if: ${{ false }}
2218
runs-on: ubuntu-latest
2319
timeout-minutes: 20
2420
env:

.github/workflows/blue-pr-sync.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
name: blue-pr-sync
22

33
on:
4-
pull_request_target:
5-
types:
6-
- opened
7-
- synchronize
8-
- reopened
9-
- edited
4+
workflow_dispatch:
105

116
permissions:
127
contents: read
138
pull-requests: read
149

1510
concurrency:
16-
group: blue-pr-sync-${{ github.event.pull_request.number }}
11+
group: blue-pr-sync-${{ github.event.pull_request.number || github.run_id }}
1712
cancel-in-progress: true
1813

1914
jobs:
2015
gate:
16+
# Temporarily disabled while blue submissions are inactive.
17+
if: ${{ false }}
2118
runs-on: ubuntu-latest
2219
timeout-minutes: 10
2320
outputs:

.github/workflows/trusted-blue-eval.yml

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,22 @@ on:
1212
required: false
1313
default: true
1414
type: boolean
15-
# Auto-fire after blue-pr-sync registers a new patch in the API queue.
16-
# The claim step returns HTTP 204 when the queue is empty, so spurious
17-
# triggers are no-ops.
18-
workflow_run:
19-
workflows: ["blue-pr-sync"]
20-
types: [completed]
2115

2216
permissions:
2317
contents: read
2418

2519
env:
2620
KERNELGUARD_API_BASE_URL: https://kguard.sinatras.dev
2721

28-
# Per-trigger concurrency group so multiple PRs syncing within seconds of
29-
# each other do not cancel each other out (GitHub allows only 1 pending +
30-
# 1 running per group, beyond which new runs auto-cancel). Each
31-
# blue-pr-sync run carries the candidate head SHA on
32-
# event.workflow_run.head_sha; manual workflow_dispatch falls back to the
33-
# unique run id so manual drains never collide with each other either.
22+
# Manual workflow_dispatch falls back to the unique run id.
3423
concurrency:
3524
group: trusted-blue-eval-${{ github.event.workflow_run.head_sha || github.run_id }}
3625
cancel-in-progress: false
3726

3827
jobs:
3928
claim:
40-
# Skip when triggered by a failed blue-pr-sync (rejected gate, static
41-
# security failure, etc.). Manual workflow_dispatch always passes this
42-
# check because workflow_run context is empty in that case.
43-
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
29+
# Temporarily disabled while blue submissions are inactive.
30+
if: ${{ false }}
4431
runs-on: ubuntu-latest
4532
timeout-minutes: 10
4633
environment: trusted-blue-eval

kernelguard.py

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2846,6 +2846,149 @@ def detect_hardcoded_shapes(code: str | SubmissionFacts) -> list[dict]:
28462846
return matches
28472847

28482848

2849+
def detect_benchmark_unsafe_algo_dispatch(code: str | SubmissionFacts) -> list[dict]:
2850+
"""Shape/data-family dispatch into unsafe specialized algorithms.
2851+
2852+
Per-shape tuning is legitimate. This detector requires the stronger
2853+
combination: benchmark-shaped branches plus data-family classifiers and
2854+
fast-path terms that imply reduced precision, truncated work, or a generic
2855+
exact fallback for cases outside the public fixture.
2856+
"""
2857+
facts = ensure_submission_facts(code)
2858+
raw_code = facts.raw_code
2859+
active_code = facts.python_active
2860+
combined = f"{active_code}\n{raw_code}"
2861+
low = combined.lower()
2862+
matches = []
2863+
2864+
shape_gate_count = len(re.findall(
2865+
r'(?:\(\s*[Bb]\s*,\s*[Nn]\s*\)\s*==\s*\(\s*\d{1,5}\s*,\s*\d{1,5}\s*\)|'
2866+
r'\b(?:B|b|batch|N|n)\s*(?:==|!=|<=|>=|<|>)\s*\d{1,5})',
2867+
combined,
2868+
))
2869+
benchmark_constant_count = len(re.findall(
2870+
r'\b(?:60|176|352|512|640|768|1024|2048|4096|1280)\b',
2871+
combined,
2872+
))
2873+
family_terms = re.findall(
2874+
r'\b(?:classify_512|classify_1024|detect_label|rankdef|rank_def|clustered|'
2875+
r'nearrank|near_rank|nearcol|near_col|mixed|rowscale|colnorm|zerofrac|'
2876+
r'colrange|tail_max|neardiff|inactive_tail)\b',
2877+
low,
2878+
)
2879+
unsafe_terms = re.findall(
2880+
r'\b(?:zero_tail|trunc|truncate|stop_at|switch_at|ncap|ncols|limit|'
2881+
r'_trunc_stop|tf32|fp16|bf16|fast_path|unsafe|rankdef)\b',
2882+
low,
2883+
)
2884+
fallback_terms = re.findall(
2885+
r'\b(?:torch\.geqrf|torch\.linalg\.(?:qr|lu)|fallback|fall back|exact|generic)\b',
2886+
combined,
2887+
re.IGNORECASE,
2888+
)
2889+
raw_native_cache = (
2890+
bool(re.search(r'\bnvcc\b', low))
2891+
and bool(re.search(r'\b(?:ctypes|CDLL|subprocess\.run|Popen|\.so|\.cu)\b', combined))
2892+
and not bool(re.search(r'\bload_inline\s*\(', active_code))
2893+
and len(re.findall(r'\b(?:data_ptr|ctypes\.c_void_p|uint64_t|void\s*\*)\b', combined)) >= 4
2894+
)
2895+
2896+
strong_shape_fixture = shape_gate_count >= 8 and benchmark_constant_count >= 20
2897+
data_family_routing = len(family_terms) >= 4 and len(unsafe_terms) >= 4
2898+
exact_escape_hatch = len(fallback_terms) >= 2
2899+
2900+
if raw_native_cache and strong_shape_fixture and data_family_routing:
2901+
signals = [
2902+
"raw native compile/load cache",
2903+
f"{shape_gate_count} shape gates",
2904+
f"{len(set(family_terms))} data-family terms",
2905+
f"{len(set(unsafe_terms))} unsafe/truncation terms",
2906+
]
2907+
if exact_escape_hatch:
2908+
signals.append("exact/generic fallback path")
2909+
matches.append({
2910+
"pattern": "BENCHMARK_UNSAFE_ALGO_DISPATCH",
2911+
"severity": "high",
2912+
"evidence": "; ".join(signals),
2913+
})
2914+
2915+
return matches
2916+
2917+
2918+
def detect_input_structure_truncation_dispatch(code: str | SubmissionFacts) -> list[dict]:
2919+
"""Input probes routing to partial computation or exact subset repair.
2920+
2921+
This is not a ban on algorithm selection. It targets classifiers/probes of
2922+
input structure that steer only a detected subset into truncated fast paths,
2923+
tail-zeroing, or reference/exact repair, which is the static shape of a
2924+
public-fixture assumption.
2925+
"""
2926+
facts = ensure_submission_facts(code)
2927+
active_code = facts.python_active
2928+
raw_code = facts.raw_code
2929+
combined = f"{active_code}\n{raw_code}"
2930+
low = combined.lower()
2931+
matches = []
2932+
2933+
probe_terms = re.findall(
2934+
r'\b(?:probe_cond|probe_n|_probe|_probe_conditioning|_s3_collinear|'
2935+
r'spread_thresh|sparse_thresh|corr_thresh|bit0|bit1|classify_512|'
2936+
r'classify_1024|detect_label|colnorm|neardiff|zerofrac|tail_max)\b',
2937+
low,
2938+
)
2939+
subset_terms = re.findall(
2940+
r'\b(?:bad_idx|good_idx|bad_count|good_count|index_select|index_copy|'
2941+
r'scatter_exact|compact_label|panel_labels|label_bad_cache|gather_.*bad|'
2942+
r'compact_.*bad)\b',
2943+
low,
2944+
)
2945+
partial_terms = re.findall(
2946+
r'(?:tau\s*\[[^\]]*limit\s*:\s*\]\s*=\s*0|H\s*\[[^\]]*limit\s*:\s*\]\s*=\s*0|'
2947+
r'\b(?:zero_tail|trunc|truncate|inactive_tail|stop_at|switch_at|ncap|ncols|limit\s*<\s*n)\b)',
2948+
combined,
2949+
re.IGNORECASE,
2950+
)
2951+
repair_terms = re.findall(
2952+
r'\b(?:repair|exact|torch\.geqrf|torch\.linalg\.(?:qr|lu)|index_copy|index_select)\b',
2953+
combined,
2954+
re.IGNORECASE,
2955+
)
2956+
probe_set = set(probe_terms)
2957+
subset_set = set(subset_terms)
2958+
partial_set = set(partial_terms)
2959+
repair_set = set(repair_terms)
2960+
2961+
probe_driven_repair = (
2962+
bool(probe_set & {"probe_cond", "probe_n", "_probe", "_s3_collinear"})
2963+
and len(probe_set) >= 4
2964+
and len(subset_set) >= 3
2965+
and len(repair_set) >= 3
2966+
)
2967+
classifier_driven_partial = (
2968+
bool(probe_set & {"classify_512", "classify_1024", "detect_label"})
2969+
and len(probe_set) >= 4
2970+
and len(partial_set) >= 3
2971+
and len(subset_set) >= 2
2972+
)
2973+
2974+
if probe_driven_repair or classifier_driven_partial:
2975+
signals = [
2976+
f"{len(probe_set)} structure/probe terms",
2977+
f"{len(subset_set)} subset-routing terms",
2978+
]
2979+
if partial_set:
2980+
signals.append(f"{len(partial_set)} partial/truncation terms")
2981+
if repair_set:
2982+
signals.append(f"{len(repair_set)} exact-repair terms")
2983+
matches.append({
2984+
"pattern": "INPUT_STRUCTURE_TRUNCATION_DISPATCH",
2985+
"severity": "high",
2986+
"evidence": "; ".join(signals),
2987+
})
2988+
2989+
return matches
2990+
2991+
28492992
def detect_unsync_multistream(code: str | SubmissionFacts) -> list[dict]:
28502993
"""Pattern 14: Unsynchronized multi-stream dispatch to exploit timing.
28512994
@@ -7289,6 +7432,14 @@ class RulePolicy:
72897432
"HARDCODED_SHAPES", "performance_heuristic", "telemetry", TELEMETRY_ONLY, (),
72907433
(), "downgrade",
72917434
),
7435+
"BENCHMARK_UNSAFE_ALGO_DISPATCH": RulePolicy(
7436+
"BENCHMARK_UNSAFE_ALGO_DISPATCH", "fixture_specialization", "hard", AUTO_FILTER, (),
7437+
(), "keep",
7438+
),
7439+
"INPUT_STRUCTURE_TRUNCATION_DISPATCH": RulePolicy(
7440+
"INPUT_STRUCTURE_TRUNCATION_DISPATCH", "fixture_specialization", "hard", AUTO_FILTER, (),
7441+
(), "keep",
7442+
),
72927443
"UNSYNC_MULTISTREAM": RulePolicy(
72937444
"UNSYNC_MULTISTREAM", "timing_manipulation", "telemetry", TELEMETRY_ONLY, (),
72947445
(), "downgrade",
@@ -7556,6 +7707,8 @@ def support_only_patterns(matched_patterns: list[dict]) -> bool:
75567707
detect_torch_compile_cache,
75577708
detect_cuda_graph_python,
75587709
detect_hardcoded_shapes,
7710+
detect_benchmark_unsafe_algo_dispatch,
7711+
detect_input_structure_truncation_dispatch,
75597712
detect_unsync_multistream,
75607713
detect_cuda_event_disable_timing,
75617714
detect_token_paste_cuda_api,
@@ -7617,6 +7770,8 @@ def support_only_patterns(matched_patterns: list[dict]) -> bool:
76177770
("torch_compile_cache", detect_torch_compile_cache),
76187771
("cuda_graph_python", detect_cuda_graph_python),
76197772
("hardcoded_shapes", detect_hardcoded_shapes),
7773+
("benchmark_unsafe_algo_dispatch", detect_benchmark_unsafe_algo_dispatch),
7774+
("input_structure_truncation_dispatch", detect_input_structure_truncation_dispatch),
76207775
("unsync_multistream", detect_unsync_multistream),
76217776
("cuda_event_disable_timing", detect_cuda_event_disable_timing),
76227777
("token_paste_cuda_api", detect_token_paste_cuda_api),
@@ -8522,7 +8677,7 @@ def _worker_parquet(args: tuple) -> dict:
85228677
"RUNNER_PLAN_CACHE", "CUDA_GRAPH_PYTHON", "CUDA_GRAPH_REPLAY",
85238678
"TIMER_MONKEYPATCH", "FAKE_BENCHMARK_EMIT", "STDIO_REDIRECT", "UNSYNC_MULTISTREAM", "CUDA_EVENT_DISABLE_TIMING",
85248679
"SCALED_MM_REF", "DECODE_MM_REF", "SILENT_FALLBACK", "REFERENCE_PRECOMPUTE_REPLAY", "TORCH_COMPILE_CACHE",
8525-
"HARDCODED_SHAPES", "TRIVIAL_PROBE",
8680+
"HARDCODED_SHAPES", "BENCHMARK_UNSAFE_ALGO_DISPATCH", "INPUT_STRUCTURE_TRUNCATION_DISPATCH", "TRIVIAL_PROBE",
85268681
"OBFUSCATED_EXEC", "DYNAMIC_EXECUTION", "MODULE_RELOAD", "THREAD_INJECTION", "LAZY_TENSOR",
85278682
"TOKEN_PASTE_CUDA_API", "SEQUENCE_BATCH_GRAPH", "PARTIAL_GRAPH_KEY", "RUNTIME_PACKAGE_INSTALL",
85288683
"PRECISION_DOWNGRADE", "SCORE_PHYSICS_FLOOR", "SCORE_IMPOSSIBLE", "SCORE_SUSPECT_FLOOR",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "kernelguard"
7-
version = "0.3.0"
7+
version = "0.3.1"
88
description = "Rule-based GPU kernel hack detector."
99
readme = "README.md"
1010
requires-python = ">=3.11"

0 commit comments

Comments
 (0)