Skip to content

fix(detector): catch LAST_CALL_REPLAY in closure-returned inner functions#13

Closed
SinatrasC wants to merge 1 commit into
mainfrom
blue-closure-replay-kg-red-14
Closed

fix(detector): catch LAST_CALL_REPLAY in closure-returned inner functions#13
SinatrasC wants to merge 1 commit into
mainfrom
blue-closure-replay-kg-red-14

Conversation

@SinatrasC

Copy link
Copy Markdown
Collaborator

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.2
classified the exploit valid because the is data: return saved_out logic
lives in a closure-returned inner function bound to custom_kernel only at
module load:

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()

Same exploit shape as textbook LAST_CALL_REPLAY; only the storage
location (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_replay
to collect entrypoint aliases:

  • custom_kernel = some_function → alias = {some_function}
  • custom_kernel = factory() → factory is inspected for top-level Returns; any returned Name is added to the alias set

The main entrypoint loop now accepts either is_entrypoint_name(node.name)
or node.name in entrypoint_aliases, so the existing identity-replay
passes fire on the inner function as if it were named custom_kernel.
_looks_stateful_name already matches last_in/last_out via 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) → flagged LAST_CALL_REPLAY, should_filter=True
  • Existing classic last_call_replay (module globals) → still flagged
  • legit_factory_kernel (return data + 1) → stays valid
  • legit_factory_with_buffer (if buf is None: buf = empty_like; return buf + data) → stays valid (workspace pattern, body has calls)
  • legit_decorator_kernel (no replay logic) → stays valid
  • plain_kernel → stays valid
  • tests/test_replay_regressions.py regressions pass (excluding the pre-existing strict-profile config test failure unrelated to detector logic)

Decorator-wrapped replay (@_replay_cache def custom_kernel) is a
separate FN that this PR does not address — left for a follow-up.

… 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.
@SinatrasC SinatrasC temporarily deployed to kernelguard-api-control-plane May 1, 2026 11:16 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown

KernelGuard Blue Evaluation

@SinatrasC

Copy link
Copy Markdown
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.

@SinatrasC SinatrasC closed this Jun 20, 2026
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.

1 participant