fix(detector): catch LAST_CALL_REPLAY in closure-returned inner functions#13
Closed
SinatrasC wants to merge 1 commit into
Closed
fix(detector): catch LAST_CALL_REPLAY in closure-returned inner functions#13SinatrasC wants to merge 1 commit into
SinatrasC wants to merge 1 commit into
Conversation
… closure-returned inner function
Targets the case where the publicly-exposed custom_kernel is bound at
module level to a function returned from a factory:
def _make_kernel():
last_in = None
last_out = None
def k(data):
nonlocal last_in, last_out
if last_in is data:
return last_out
last_in = data
last_out = data.clone()
return last_out
return k
custom_kernel = _make_kernel()
The replay logic lives in 'k', not in any function literally named
custom_kernel, so the existing entrypoint matcher
(is_entrypoint_name(node.name)) skipped it entirely. Same exploit
semantics as the textbook LAST_CALL_REPLAY, only the storage moves from
module globals to closure cells.
Approach: walk the module top-level once before the existing entrypoint
loop, collect any function names that are aliased to a known entrypoint
name via either
custom_kernel = some_function # direct alias
custom_kernel = factory() # factory call
For factory calls, the factory is scanned for top-level Return statements
and any returned Name is added to the alias set. The main entrypoint loop
then accepts both is_entrypoint_name(node.name) and node.name in
entrypoint_aliases, so the existing identity-replay passes fire on the
inner function as if it were custom_kernel. _looks_stateful_name already
matches 'last_in'/'last_out' via the 'last' substring, so no other
detector logic needed changing.
Verified: closure_replay flagged, decorator_replay still misses (separate
pattern, separate fix), and all benign factory/decorator/plain shapes
stay valid. Existing classic LAST_CALL_REPLAY regression tests pass.
KernelGuard Blue Evaluation
|
9 tasks
Collaborator
Author
|
Thanks for the KernelGuard Flywheel Campaign contribution. We are not merging this narrow variant separately because the consolidated rule-family implementation in #273 is the merge path for this detector area. |
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.
Summary
Targets red #14 — submitted via the new Flywheel-proxied red-submission
flow on the campaign root (no participant KG API key). Live
kernelguard==0.2.2classified the exploit
validbecause theis data: return saved_outlogiclives in a closure-returned inner function bound to
custom_kernelonly atmodule load:
Same exploit shape as textbook
LAST_CALL_REPLAY; only the storagelocation (closure cells vs module globals) and the entrypoint binding
(factory return vs
def custom_kernel) changed.Approach
Walk the module top-level once at the start of
detect_last_call_replayto collect entrypoint aliases:
custom_kernel = some_function→ alias ={some_function}custom_kernel = factory()→ factory is inspected for top-levelReturns; any returnedNameis added to the alias setThe main entrypoint loop now accepts either
is_entrypoint_name(node.name)or
node.name in entrypoint_aliases, so the existing identity-replaypasses fire on the inner function as if it were named
custom_kernel._looks_stateful_namealready matcheslast_in/last_outvia the"last"substring, so no other detector logic needed changing.KernelGuard-Red-Submission: 14
Test plan
closure_replay(red fix(detector): catch LAST_CALL_REPLAY via cursor-indexed reference replay #14) → flaggedLAST_CALL_REPLAY,should_filter=Truelast_call_replay(module globals) → still flaggedlegit_factory_kernel(return data + 1) → staysvalidlegit_factory_with_buffer(if buf is None: buf = empty_like; return buf + data) → staysvalid(workspace pattern, body has calls)legit_decorator_kernel(no replay logic) → staysvalidplain_kernel→ staysvalidtests/test_replay_regressions.pyregressions pass (excluding the pre-existing strict-profile config test failure unrelated to detector logic)Decorator-wrapped replay (
@_replay_cache def custom_kernel) is aseparate FN that this PR does not address — left for a follow-up.