Files
Open_Ultra_SAST/tests/test_obligation_cfg_dominance.py
T
Marius CiepluchandClaude Fable 5.1 03b114cbeb feat(prover): delivery D: control flow decides dominance; the trace carries each guard's relation, effect and the operation's control context
Opus agent delivery under spec access-control-prover, requirement 10. `semantic/cfg.py` builds a control-flow
graph per function from the syntax tree (if/else, switch arms, loops, try/catch/finally, return, throw, break,
continue, exit, assert) with dominators and a per-line control context, and leaves a function without a graph
rather than a wrong one. `obligations/cfg_dominance.py::CfgDominance` discharges a statement guard only when it
lies on every path to the operation and the operation is unreachable from its failing arm; a predicate whose
result nothing consumes is `value_only` and never discharges; router, decorator and earlier-hop witnesses behave
as before; without a graph the order rule applies and `cfg_unavailable` is recorded. The trace's guards carry
`relation` and `effect`, the trace carries `operation_context`; Java parameter lists carry names only; a
handler-named operation's arguments are the handler's parameters. The checker's default stays order-based.

Smoke on the 23 fix changes: 15 caught, 55 of 93 locations, precision 15 of 15, no failure, no change over
120 s (unchanged from delivery B). Gate: 3575 tests pass; the reserved-repository guard passes on the export.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 19:01:07 +02:00

114 lines
6.4 KiB
Python

"""CfgDominance: a guard counts only when control flow keeps the operation from running after it fails."""
import pytest
from openultrasast.semantic.cfg import FunctionCfg
from openultrasast.semantic.cst import parse_function_nodes
from openultrasast.semantic.extra import grammar_for
from openultrasast.semantic.obligations.cfg_dominance import CfgDominance, describe, operation_context
from openultrasast.semantic.obligations.dominance import Dominance, OrderDominance
from openultrasast.semantic.obligations.operations import Discharge, Operation
from openultrasast.semantic.obligations.paths import PathRecord
PATH = "T.java"
def setup(body, kind="ownership_check", marker="can("):
if grammar_for("java") is None:
pytest.skip("no Java grammar installed")
text = "class T {\n Object handle(Item item) {\n" + "".join(f" {line}\n" for line in body) + " }\n}\n"
assert len(text.encode()) > 0
nodes = parse_function_nodes(PATH, text, "java")
assert nodes, "the parser read no function"
cfg = FunctionCfg.from_cst(nodes[0], "java")
lines = text.splitlines()
guard_line = next(number for number, line in enumerate(lines, 1) if marker in line)
op_line = next(number for number, line in enumerate(lines, 1) if "read(" in line)
witness = Discharge(PATH, guard_line, "handle", kind, "guard", "authenticated_context", "statement")
operation = Operation(PATH, op_line, "handle", "protected_read", "read", None, ("path_guard", "ownership_check"))
return CfgDominance({PATH: text}, {(PATH, "handle"): cfg}), witness, operation, text
def test_protocol():
dominance, *_ = setup(["read(item);"], marker="read(")
assert isinstance(dominance, Dominance)
def test_value_only_predicate_does_not_discharge():
dominance, witness, operation, _ = setup(["boolean ok = can(item);", "return read(item);"])
assert dominance.relation(witness, operation) == "all_paths"
assert dominance.effect(witness, operation) == "value_only"
assert not dominance.dominates(witness, operation, None)
def test_same_predicate_guarding_a_throw_discharges():
dominance, witness, operation, _ = setup(["if (!can(item)) {", " throw new AccessDeniedException();", "}", "return read(item);"])
assert (dominance.relation(witness, operation), dominance.effect(witness, operation)) == ("all_paths", "throws")
assert dominance.dominates(witness, operation, None)
def test_operation_only_inside_passing_branch_discharges():
dominance, witness, operation, _ = setup(["if (can(item)) {", " return read(item);", "}", "return null;"])
assert dominance.effect(witness, operation) == "returns"
assert dominance.dominates(witness, operation, None)
dominance, witness, operation, _ = setup(["if (can(item)) {", " read(item);", "}"])
assert dominance.effect(witness, operation) == "branches"
assert dominance.dominates(witness, operation, None)
assert operation_context(dominance, operation).conditions == ("can(item)",)
def test_branch_only_guard_is_some_paths_and_no_discharge():
dominance, witness, operation, text = setup(
["if (item.isShared()) {", " checkPermission(ADMINISTER);", "}", "return read(item);"], kind="path_guard", marker="checkPermission"
)
assert dominance.relation(witness, operation) == "some_paths"
assert dominance.effect(witness, operation) == "throws"
assert not dominance.dominates(witness, operation, None)
# The order-based default accepts the same guard: it reads only that the guard comes first.
assert OrderDominance({PATH: text}).dominates(witness, operation, None)
def test_throwing_guard_in_swallowing_try_does_not_dominate():
dominance, witness, operation, _ = setup(
["try {", " checkPermission(ADMINISTER);", "} catch (AccessDeniedException e) {", " log(e);", "}", "return read(item);"],
kind="path_guard",
marker="checkPermission",
)
assert not dominance.dominates(witness, operation, None)
def test_scopes_and_path_records_keep_their_semantics():
dominance, witness, operation, _ = setup(["boolean ok = can(item);", "return read(item);"])
for scope in ("decorator", "router"):
scoped = Discharge(PATH, None, "handle", "path_guard", "guard", "authenticated_context", scope)
assert dominance.dominates(scoped, operation, None)
assert describe(dominance, scoped, operation, {}) == ("all_paths", "exits")
caller = Discharge("Caller.java", 3, "entry", "path_guard", "guard", "authenticated_context", "statement")
record = PathRecord((("Caller.java::entry", 4), (f"{PATH}::handle", None)), "Caller.java::entry", True)
assert dominance.dominates(caller, operation, record)
assert not dominance.dominates(caller, operation, None)
later = PathRecord(((f"{PATH}::handle", 3), ("Caller.java::entry", None)), f"{PATH}::handle", True)
assert not dominance.dominates(caller, operation, later)
def test_missing_graph_falls_back_to_order_and_is_recorded():
text = "class T {\n Object handle(Item item) {\n checkPermission(ADMINISTER);\n return read(item);\n }\n}\n"
witness = Discharge(PATH, 3, "handle", "path_guard", "guard", "authenticated_context", "statement")
operation = Operation(PATH, 4, "handle", "protected_read", "read", None, ("path_guard",))
dominance = CfgDominance({PATH: text}, {})
assert dominance.dominates(witness, operation, None) == OrderDominance({PATH: text}).dominates(witness, operation, None)
assert [(item["reason"], item["path"], item["function"]) for item in dominance.degradations] == [("cfg_unavailable", PATH, "handle")]
unavailable = FunctionCfg(1, 6, False, "labelled_jump")
dominance = CfgDominance({PATH: text}, {(PATH, "handle"): unavailable})
assert dominance.dominates(witness, operation, None)
assert dominance.degradations[0]["detail"] == "labelled_jump"
assert not {"language", "framework", "repository"} & set(dominance.degradations[0])
def test_order_description_without_graph():
text = "def f():\n ok = can()\n if not ok:\n return\n read()\n"
witness = Discharge("a.py", 2, "f", "ownership_check", "guard", "authenticated_context", "statement")
operation = Operation("a.py", 5, "f", "protected_read", "read", None, ("ownership_check",))
assert describe(OrderDominance({"a.py": text}), witness, operation, {"a.py": text}) == ("all_paths", "returns")
assert operation_context(OrderDominance({"a.py": text}), operation) == operation_context(OrderDominance({}), operation)