mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 17:58:18 +00:00
feat(access-control-prover): 9.2 monotone summaries over recursive call components
This commit is contained in:
@@ -11,8 +11,8 @@ from openultrasast.semantic.ir import CallSite, FunctionIR
|
||||
from openultrasast.semantic.obligations import summaries
|
||||
from openultrasast.semantic.obligations.abstract_domain import Completeness
|
||||
from openultrasast.semantic.obligations.operations import Discharge, Operation
|
||||
from openultrasast.semantic.obligations.resolution import SymbolIndex
|
||||
from openultrasast.semantic.obligations.summaries import FunctionSummary, local_summaries
|
||||
from openultrasast.semantic.obligations.resolution import SymbolId, SymbolIndex
|
||||
from openultrasast.semantic.obligations.summaries import FunctionSummary, local_summaries, monotone_summaries
|
||||
|
||||
PHP_CLASS = """<?php
|
||||
class InvoiceController {
|
||||
@@ -43,6 +43,61 @@ class Gate {
|
||||
}
|
||||
"""
|
||||
|
||||
JAVA_SELF_LOOP = """public class Node {
|
||||
int walk() {
|
||||
return this.walk();
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
JAVA_MUTUAL = """public class C {
|
||||
void a() {
|
||||
this.b();
|
||||
}
|
||||
void b() {
|
||||
this.a();
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
JAVA_CHAIN = """public class Chain {
|
||||
void helper1() {
|
||||
this.helper2();
|
||||
}
|
||||
void helper2() {
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
JAVA_THREE_CYCLE = """public class Ring {
|
||||
void a() {
|
||||
this.b();
|
||||
}
|
||||
void b() {
|
||||
this.c();
|
||||
}
|
||||
void c() {
|
||||
this.a();
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _self_call(name: str, line: int, arity: int = 0) -> CallSite:
|
||||
return CallSite(name, line, (), (), (), False, 0, receiver="this", arity=arity)
|
||||
|
||||
|
||||
def _named_operation(path: str, function_name: str, resource: str) -> Operation:
|
||||
return Operation(
|
||||
path=path,
|
||||
line=3,
|
||||
function=function_name,
|
||||
kind="protected_read",
|
||||
fact_id="java:protected-read",
|
||||
resource=resource,
|
||||
requires=("path_guard",),
|
||||
)
|
||||
|
||||
|
||||
def _scope_functions(fixture: str, path: str, language: str) -> tuple[FunctionIR, ...]:
|
||||
scopes = declaration_scopes(path, fixture, language)
|
||||
@@ -176,3 +231,119 @@ def test_function_summary_is_frozen() -> None:
|
||||
summary = FunctionSummary.__new__(FunctionSummary)
|
||||
with pytest.raises(Exception, match="cannot assign|frozen"):
|
||||
summary.symbol = None # type: ignore[misc,assignment]
|
||||
|
||||
|
||||
def _symbol_by_name(summaries: tuple[FunctionSummary, ...], name: str) -> SymbolId:
|
||||
for summary in summaries:
|
||||
if summary.symbol.name == name:
|
||||
return summary.symbol
|
||||
raise AssertionError(f"no summary named {name}")
|
||||
|
||||
|
||||
def test_direct_recursion_terminates_with_stable_operations() -> None:
|
||||
index = _index((("src/Node.java", JAVA_SELF_LOOP, "java"),))
|
||||
walk = FunctionIR("walk", (), 2, 4, (), (_self_call("walk", 3),))
|
||||
functions = {"src/Node.java": (walk,)}
|
||||
local = local_summaries(functions, index)
|
||||
operations = {_symbol_by_name(local, "walk"): (_named_operation("src/Node.java", "walk", "node"),)}
|
||||
result = monotone_summaries(functions, index, operations_by_symbol=operations)
|
||||
assert len(result) == 1
|
||||
assert result[0].operations == (_named_operation("src/Node.java", "walk", "node"),)
|
||||
assert result[0].completeness == Completeness(is_complete=True)
|
||||
|
||||
|
||||
def test_mutual_recursion_shares_accumulated_operations_and_witnesses() -> None:
|
||||
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
|
||||
function_a = FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),))
|
||||
function_b = FunctionIR("b", (), 5, 7, (), (_self_call("a", 6),))
|
||||
functions = {"src/C.java": (function_a, function_b)}
|
||||
local = local_summaries(functions, index)
|
||||
symbol_b = _symbol_by_name(local, "b")
|
||||
operations = {symbol_b: (_named_operation("src/C.java", "b", "shared"),)}
|
||||
witnesses = {symbol_b: (_witness("b"),)}
|
||||
result = monotone_summaries(functions, index, operations_by_symbol=operations, witnesses_by_symbol=witnesses)
|
||||
shared = (_named_operation("src/C.java", "b", "shared"),)
|
||||
by_name = {summary.symbol.name: summary for summary in result}
|
||||
assert by_name["a"].operations == shared
|
||||
assert by_name["b"].operations == shared
|
||||
assert by_name["a"].witnesses == (_witness("b"),)
|
||||
|
||||
|
||||
def test_chain_helper_propagates_operations_to_caller() -> None:
|
||||
index = _index((("src/Chain.java", JAVA_CHAIN, "java"),))
|
||||
helper1 = FunctionIR("helper1", (), 2, 4, (), (_self_call("helper2", 3),))
|
||||
helper2 = FunctionIR("helper2", (), 5, 6, (), ())
|
||||
functions = {"src/Chain.java": (helper1, helper2)}
|
||||
local = local_summaries(functions, index)
|
||||
operations = {_symbol_by_name(local, "helper2"): (_named_operation("src/Chain.java", "helper2", "chain"),)}
|
||||
result = monotone_summaries(functions, index, operations_by_symbol=operations)
|
||||
by_name = {summary.symbol.name: summary for summary in result}
|
||||
assert by_name["helper1"].operations == (_named_operation("src/Chain.java", "helper2", "chain"),)
|
||||
assert by_name["helper1"].completeness == Completeness(is_complete=True)
|
||||
|
||||
|
||||
def test_candidate_edges_never_propagate_operations() -> None:
|
||||
index = _index((("src/Auditor.java", JAVA_TWO_CLASSES, "java"),))
|
||||
review_calls = (CallSite("check", 2, (), (), (), False, 0, receiver="auditor", arity=0),)
|
||||
review = FunctionIR("review", (), 2, 4, (), review_calls)
|
||||
check = FunctionIR("check", (), 2, 4, (), ())
|
||||
functions = {"src/Auditor.java": (review, check)}
|
||||
local = local_summaries(functions, index)
|
||||
auditor_symbol = next(symbol for symbol in (s.symbol for s in local) if symbol.enclosing_type == "Auditor")
|
||||
operations = {auditor_symbol: (_named_operation("src/Auditor.java", "check", "auditor"),)}
|
||||
result = monotone_summaries(functions, index, operations_by_symbol=operations)
|
||||
by_name = {(summary.symbol.enclosing_type, summary.symbol.name): summary for summary in result}
|
||||
assert by_name[(None, "review")].operations == ()
|
||||
assert by_name[("Auditor", "check")].operations == (_named_operation("src/Auditor.java", "check", "auditor"),)
|
||||
|
||||
|
||||
def test_incomplete_callee_reason_propagates_to_caller() -> None:
|
||||
index = _index((("src/Chain.java", JAVA_CHAIN, "java"),))
|
||||
helper1 = FunctionIR("helper1", (), 2, 4, (), (_self_call("helper2", 3),))
|
||||
helper2 = FunctionIR("helper2", (), 5, 6, (), (CallSite("missing", 6, (), (), (), False, 0),))
|
||||
functions = {"src/Chain.java": (helper1, helper2)}
|
||||
result = monotone_summaries(functions, index)
|
||||
by_name = {summary.symbol.name: summary for summary in result}
|
||||
assert by_name["helper2"].completeness.is_complete is False
|
||||
assert by_name["helper1"].completeness.is_complete is False
|
||||
assert "resolution:no_candidate" in by_name["helper1"].completeness.reasons
|
||||
|
||||
|
||||
def test_three_cycle_accumulates_all_operations() -> None:
|
||||
index = _index((("src/Ring.java", JAVA_THREE_CYCLE, "java"),))
|
||||
function_a = FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),))
|
||||
function_b = FunctionIR("b", (), 5, 7, (), (_self_call("c", 6),))
|
||||
function_c = FunctionIR("c", (), 8, 10, (), (_self_call("a", 9),))
|
||||
functions = {"src/Ring.java": (function_a, function_b, function_c)}
|
||||
local = local_summaries(functions, index)
|
||||
operations = {
|
||||
_symbol_by_name(local, "a"): (_named_operation("src/Ring.java", "a", "ring-a"),),
|
||||
_symbol_by_name(local, "b"): (_named_operation("src/Ring.java", "b", "ring-b"),),
|
||||
_symbol_by_name(local, "c"): (_named_operation("src/Ring.java", "c", "ring-c"),),
|
||||
}
|
||||
result = monotone_summaries(functions, index, operations_by_symbol=operations)
|
||||
accumulated = (
|
||||
_named_operation("src/Ring.java", "a", "ring-a"),
|
||||
_named_operation("src/Ring.java", "b", "ring-b"),
|
||||
_named_operation("src/Ring.java", "c", "ring-c"),
|
||||
)
|
||||
for summary in result:
|
||||
assert summary.operations == accumulated
|
||||
|
||||
|
||||
def test_monotone_summaries_are_deterministic() -> None:
|
||||
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
|
||||
function_a = FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),))
|
||||
function_b = FunctionIR("b", (), 5, 7, (), (_self_call("a", 6),))
|
||||
functions = {"src/C.java": (function_a, function_b)}
|
||||
assert monotone_summaries(functions, index) == monotone_summaries(functions, index)
|
||||
|
||||
|
||||
def test_monotone_output_order_matches_local_summaries() -> None:
|
||||
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
|
||||
function_a = FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),))
|
||||
function_b = FunctionIR("b", (), 5, 7, (), (_self_call("a", 6),))
|
||||
functions = {"src/C.java": (function_a, function_b)}
|
||||
assert [summary.symbol for summary in monotone_summaries(functions, index)] == [
|
||||
summary.symbol for summary in local_summaries(functions, index)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user