Skip to content

Detect aliased-timer and frame-arg-indirection AUTO_FILTER evasions (TIMER_MONKEYPATCH / FRAME_WALK_MUTATION)#278

Merged
SinatrasC merged 2 commits into
gpu-mode:mainfrom
robobryce:detect-aliased-timer-and-frame-arg-mutation
Jun 27, 2026
Merged

Detect aliased-timer and frame-arg-indirection AUTO_FILTER evasions (TIMER_MONKEYPATCH / FRAME_WALK_MUTATION)#278
SinatrasC merged 2 commits into
gpu-mode:mainfrom
robobryce:detect-aliased-timer-and-frame-arg-mutation

Conversation

@robobryce

Copy link
Copy Markdown
Contributor

KernelGuard Blue Submission

KernelGuard-Red-Submission: 0

Defensive detector hardening, not a competition blue submission. Closes two confirmed AUTO_FILTER evasions; no existing rule is weakened.

Summary

This PR closes two different verified evasions of existing AUTO_FILTER rules, both of which classify clean against current main today. It complements #277 (which closed the gc__globals__ subscript route) by closing two distinct routes:

  1. TIMER_MONKEYPATCH — aliased / assembled-name timer patch.
  2. FRAME_WALK_MUTATION — frame-dict passed as an argument and mutated through the bound parameter.

Both rules are already AUTO_FILTER in RULE_REGISTRY, so this reuses the existing pattern names (no registry change). Additive only — no existing rule is weakened or removed.

Gap 1 — aliased / assembled-name timer patch evades TIMER_MONKEYPATCH

The dotted-literal form torch.cuda.Event.elapsed_time = w is already caught. This is not that form: the Event class is reached through getattr aliases and the attribute name is assembled from string fragments, so nothing literal survives for the textual regexes to match.

Minimal repro (currently should_filter: False, classification: valid):

import torch
def custom_kernel(data):
    cu = getattr(torch, "cuda")
    E = getattr(cu, "Ev" + "ent")
    m = "elapsed" + "_time"
    o = getattr(E, m)
    def w(self, other): return o(self, other) / 100.0
    setattr(E, m, w)
    return data

Cause. detect_timer_monkeypatch is purely textual — re.search over the raw source, every regex pinned to the dotted literal. getattr-aliasing plus string-concat names leave no literal token.

Fix. A new AST pass _detect_aliased_cuda_timer_patch:

  • builds a conservative 1-hop alias map for torch.cuda (x = getattr(torch,'cuda') / x = torch.cuda) and the Event class (y = getattr(x, 'Event' | 'Ev'+'ent') / y = x.Event / torch.cuda.Event);
  • folds string-concat and bare-string attribute names by reusing the existing _static_string helper (handles BinOp(+) of Constant strings, bare Constants, f-strings, .join), and additionally resolves a foldable string-valued local variable (m = "elapsed" + "_time"; setattr(E, m, w));
  • flags setattr(<base>, <name>, ...) and <base>.<attr> = ... WRITES where the base resolves to torch.cuda or the Event class and the name resolves to one of {Event, elapsed_time, synchronize}. Emits the existing TIMER_MONKEYPATCH pattern.

Additionally, the existing textual timer regexes now run over comment-stripped source (facts.python_active). A comment that merely mentions torch.cuda.Event = ... (e.g. "we never patch torch.cuda.synchronize") was a known false-positive source for these literal-pinned patterns — real assignment syntax survives the strip.

Precision rationale. Only writes are flagged. Reads and calls of elapsed_time — i.e. legitimate timing use, s.elapsed_time(e) — are never flagged. getattr(torch, 'float32') and a bare cu = torch.cuda with no timer write do not trip it. The string-variable map is conservative: a name reassigned to anything non-foldable is dropped.

Gap 2 — frame-arg indirection evades FRAME_WALK_MUTATION

The direct g[name] = fn form (write directly through a frame alias) is already caught. This passes the frame dict to a helper and writes through the parameter:

import sys
def _mutate(ns, name, fn):
    ns[name] = fn                              # write through a PARAMETER
def _i():
    g = sys._getframe(1).f_globals
    _mutate(g, "calculate_stats", wrapper)     # frame dict passed as ARG

Currently should_filter: False — only the non-filtering FRAME_WALK_ACCESS telemetry fires.

Cause. detect_introspection_exploit builds frame aliases from a single local Assign and flags writes only through that alias set. There is no interprocedural taint, so a parameter bound to f_globals at the call site is invisible.

Fix. A new helper _frame_arg_taint_mutation adds one level of argument taint: for each call to a module-local FunctionDef (resolved by name), if a positional argument is frame-dict-derived (an .f_globals/.f_locals attribute, or a local already in the frame-alias set), the matching parameter name is tainted within that function; a subscript-write param[...] = ... through a tainted parameter sets FRAME_WALK_MUTATION.

Precision rationale. _mutate(ordinary_local_dict, ...) does not taint — the argument is not frame-derived. A frame dict that is passed but not written (read-only helper, or a helper that writes a different, non-tainted parameter) remains FRAME_WALK_ACCESS — the access/mutation split is preserved. *args spillover that cannot be mapped positionally does not taint.

Files changed

  • kernelguard.py
    • detect_timer_monkeypatch: textual regexes now run over comment-stripped source; new _detect_aliased_cuda_timer_patch (1-hop torch.cuda/Event alias map + foldable string-name resolution + setattr/assignment write detection) wired in.
    • detect_introspection_exploit: new _frame_arg_taint_mutation (+ _expr_is_frame_derived, _positional_params) wired in as an additional FRAME_WALK_MUTATION trigger, gated to fire only when the existing alias tracking did not already flag a mutation.
  • tests/test_aliased_timer_and_frame_arg_mutation.py (new, stdlib unittest, no new deps): both repros, a clean precision battery, the access/mutation-split preservation case, and pre-existing-positive regression guards.

Test results

There is no pre-existing automated test suite in the repo (no pytest/tox/conftest, no test deps in uv.lock); CI prechecks run py_compile of the core entrypoints, a pip install -e ".[api]", and a lightweight API-startup smoke test. I ran the CI-equivalent checks on unmodified upstream (f01f2b5) and on this branch — identical, all pass:

Check BEFORE (upstream f01f2b5) AFTER (this branch)
py_compile kernelguard.py kernelguard_api.py kernelguard_mcp.py PASS PASS
import kernelguard PASS PASS
API smoke (kernelguard_api.build_app() routes ['/analyze', '/health']) PASS PASS
Static security gate (scripts/github/blue_pr_static_security.py) passed: true, findings: [] passed: true, findings: []

New test suite (python -m unittest discover -s tests): 16 passed, 0 failed.

Targeted before/after (analyze_code(should_filter, classification))

sample                          BEFORE                       AFTER
gap1_aliased_timer              (False, 'valid')             (True,  'hacked')          # <<< gap closed
gap2_frame_arg_mutation         (False, 'low_confidence')    (True,  'hacked')          # <<< gap closed
clean_comment_mentions_timer    (True,  'hacked')            (False, 'low_confidence')  # <<< false positive fixed
clean_matmul                    (False, ...)                 (False, ...)
clean_eigh                      (False, 'valid')             (False, 'valid')
clean_elapsed_time_use          (False, ...)                 (False, ...)               # legitimate Event.elapsed_time() use
clean_getattr_float32           (False, ...)                 (False, ...)
clean_mutate_plain_dict         (False, 'valid')             (False, 'valid')           # _mutate(plain_dict, ...)
clean_reads_f_globals_only      (False, ...)                 (False, ...)               # reads f_globals, no write
clean_pass_frame_no_write       (False, ...)                 (False, ...)               # stays FRAME_WALK_ACCESS
clean_bare_cuda_alias           (False, ...)                 (False, ...)
existing_dotted_event_assign    (True,  'hacked')            (True,  'hacked')          # existing rule, unchanged
existing_dotted_elapsed_patch   (True,  'hacked')            (True,  'hacked')          # existing rule, unchanged
existing_direct_frame_write     (True,  'hacked')            (True,  'hacked')          # existing rule, unchanged

Exactly the two intended evasions flip to filtered (plus one pre-existing comment-only false positive is removed); every other clean kernel and every pre-existing positive is unchanged.

🤖 Generated with Claude Code

brycelelbach and others added 2 commits June 28, 2026 02:41
Close two confirmed evasions of existing AUTO_FILTER rules that classify
clean against current main. Additive only -- no existing rule is weakened.

GAP 1 (TIMER_MONKEYPATCH): aliased / assembled-name timer patch.
  detect_timer_monkeypatch is purely textual, every regex pinned to the
  dotted literal, so getattr-aliasing + string-concat names leave no token:

      cu = getattr(torch, "cuda")
      E  = getattr(cu, "Ev" + "ent")
      m  = "elapsed" + "_time"
      setattr(E, m, w)

  Add an AST pass (_detect_aliased_cuda_timer_patch) that builds a 1-hop
  alias map for torch.cuda / the Event class, folds string-concat and
  foldable string-variable names (reusing _static_string), and flags
  setattr(<base>,<name>,...) and <base>.<attr> = ... WRITES whose base
  resolves to torch.cuda or Event and whose name is one of
  {Event, elapsed_time, synchronize}. Reads/CALLS of elapsed_time
  (legitimate timing) are never flagged -- only write targets.
  Also run the existing textual regexes over comment-stripped source so a
  comment that merely mentions a timer patch no longer false-positives.

GAP 2 (FRAME_WALK_MUTATION): frame-arg indirection.
  detect_introspection_exploit builds frame aliases from a single local
  Assign only, so a frame dict passed as an argument and written through a
  parameter is invisible:

      def _mutate(ns, name, fn):
          ns[name] = fn
      g = sys._getframe(1).f_globals
      _mutate(g, "calculate_stats", wrapper)

  Add one level of argument taint (_frame_arg_taint_mutation): for each call
  to a module-local FunctionDef, a frame-derived positional argument taints
  the matching parameter, and subscript-writes through a tainted parameter
  set FRAME_WALK_MUTATION. A plain local-dict argument does not taint; a
  pass-without-write stays FRAME_WALK_ACCESS (the split is preserved).

Complements PR gpu-mode#277 (gc -> __globals__ subscript route); these are the
aliased-timer and frame-arg-indirection routes.

Tests: new tests/test_aliased_timer_and_frame_arg_mutation.py (stdlib
unittest, no new deps) -- both repros now should_filter=True, a clean
precision battery stays False, pre-existing positives unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

3 participants