mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
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]>
114 lines
6.4 KiB
Python
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)
|