Detect aliased-timer and frame-arg-indirection AUTO_FILTER evasions (TIMER_MONKEYPATCH / FRAME_WALK_MUTATION)#278
Merged
SinatrasC merged 2 commits intoJun 27, 2026
Conversation
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>
215cc00 to
6a2d991
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
KernelGuard Blue Submission
KernelGuard-Red-Submission: 0
Summary
This PR closes two different verified evasions of existing AUTO_FILTER rules, both of which classify clean against current
maintoday. It complements #277 (which closed thegc→__globals__subscript route) by closing two distinct routes:TIMER_MONKEYPATCH— aliased / assembled-name timer patch.FRAME_WALK_MUTATION— frame-dict passed as an argument and mutated through the bound parameter.Both rules are already
AUTO_FILTERinRULE_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_MONKEYPATCHThe dotted-literal form
torch.cuda.Event.elapsed_time = wis already caught. This is not that form: the Event class is reached throughgetattraliases 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):Cause.
detect_timer_monkeypatchis purely textual —re.searchover 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:torch.cuda(x = getattr(torch,'cuda')/x = torch.cuda) and theEventclass (y = getattr(x, 'Event' | 'Ev'+'ent')/y = x.Event/torch.cuda.Event);_static_stringhelper (handlesBinOp(+)ofConstantstrings, bareConstants, f-strings,.join), and additionally resolves a foldable string-valued local variable (m = "elapsed" + "_time"; setattr(E, m, w));setattr(<base>, <name>, ...)and<base>.<attr> = ...WRITES where the base resolves totorch.cudaor theEventclass and the name resolves to one of{Event, elapsed_time, synchronize}. Emits the existingTIMER_MONKEYPATCHpattern.Additionally, the existing textual timer regexes now run over comment-stripped source (
facts.python_active). A comment that merely mentionstorch.cuda.Event = ...(e.g. "we never patchtorch.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 barecu = torch.cudawith 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_MUTATIONThe direct
g[name] = fnform (write directly through a frame alias) is already caught. This passes the frame dict to a helper and writes through the parameter:Currently
should_filter: False— only the non-filteringFRAME_WALK_ACCESStelemetry fires.Cause.
detect_introspection_exploitbuilds frame aliases from a single localAssignand flags writes only through that alias set. There is no interprocedural taint, so a parameter bound tof_globalsat the call site is invisible.Fix. A new helper
_frame_arg_taint_mutationadds one level of argument taint: for each call to a module-localFunctionDef(resolved by name), if a positional argument is frame-dict-derived (an.f_globals/.f_localsattribute, or a local already in the frame-alias set), the matching parameter name is tainted within that function; a subscript-writeparam[...] = ...through a tainted parameter setsFRAME_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) remainsFRAME_WALK_ACCESS— the access/mutation split is preserved.*argsspillover that cannot be mapped positionally does not taint.Files changed
kernelguard.pydetect_timer_monkeypatch: textual regexes now run over comment-stripped source; new_detect_aliased_cuda_timer_patch(1-hoptorch.cuda/Eventalias 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 additionalFRAME_WALK_MUTATIONtrigger, 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, stdlibunittest, 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 inuv.lock); CI prechecks runpy_compileof the core entrypoints, apip 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:f01f2b5)py_compile kernelguard.py kernelguard_api.py kernelguard_mcp.pyimport kernelguardkernelguard_api.build_app()routes['/analyze', '/health'])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))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