Skip to content

Commit 3fd105e

Browse files
Detect first-call state replay
1 parent e3829c7 commit 3fd105e

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

kernelguard.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2197,11 +2197,75 @@ def _has_ver(expr: ast.AST | None) -> bool:
21972197
for n in ast.walk(expr)
21982198
)
21992199

2200+
def _input_derived(expr: ast.AST | None, params: set[str]) -> bool:
2201+
return bool(_expr_names(expr) & params)
2202+
22002203
for node in ast.walk(tree):
22012204
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
22022205
continue
22032206
if not is_entrypoint_name(node.name):
22042207
continue
2208+
params = {arg.arg for arg in node.args.args}
2209+
2210+
# First-call state replay:
2211+
# if _state is None:
2212+
# _state = compute(input)
2213+
# return _state
2214+
#
2215+
# Or the inverse fast path:
2216+
# if _state is not None:
2217+
# return _state
2218+
# _state = compute(input)
2219+
# return _state
2220+
#
2221+
# The guard is input-independent, the slot appears in that guard, and
2222+
# the returned slot is assigned from a live input in the entrypoint.
2223+
for child in ast.walk(node):
2224+
if not isinstance(child, ast.If):
2225+
continue
2226+
if _expr_names(child.test) & params:
2227+
continue
2228+
if child not in node.body:
2229+
continue
2230+
2231+
test_roots = _expr_names(child.test)
2232+
returned_in_body = {
2233+
_ast_root_name(stmt.value)
2234+
for stmt in child.body
2235+
if isinstance(stmt, ast.Return) and stmt.value is not None
2236+
} - {None}
2237+
assigned_from_input_in_body = {
2238+
_ast_root_name(target)
2239+
for stmt in child.body
2240+
if isinstance(stmt, ast.Assign) and _input_derived(stmt.value, params)
2241+
for target in stmt.targets
2242+
} - {None}
2243+
later_statements = node.body[node.body.index(child) + 1:]
2244+
returned_after_if = {
2245+
_ast_root_name(stmt.value)
2246+
for stmt in later_statements
2247+
if isinstance(stmt, ast.Return) and stmt.value is not None
2248+
} - {None}
2249+
assigned_after_if = {
2250+
_ast_root_name(target)
2251+
for stmt in later_statements
2252+
if isinstance(stmt, ast.Assign) and _input_derived(stmt.value, params)
2253+
for target in stmt.targets
2254+
} - {None}
2255+
2256+
init_then_return = assigned_from_input_in_body & returned_after_if
2257+
fast_return_then_init = returned_in_body & assigned_after_if & returned_after_if
2258+
replay_roots = (init_then_return | fast_return_then_init) & test_roots
2259+
if replay_roots:
2260+
root = next(iter(replay_roots))
2261+
return [{
2262+
"pattern": "LAST_CALL_REPLAY",
2263+
"severity": "critical",
2264+
"evidence": (
2265+
f"{entrypoint_name} returns first-call output from "
2266+
f"input-independent state slot {root}"
2267+
),
2268+
}]
22052269

22062270
signature_features: dict[str, set[str]] = defaultdict(set)
22072271
saved_state_features: dict[str, set[str]] = defaultdict(set)

0 commit comments

Comments
 (0)