coretrace-python-analyzer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coretrace_python/__init__.py +4 -0
- coretrace_python/__main__.py +4 -0
- coretrace_python/abstract/__init__.py +45 -0
- coretrace_python/abstract/constants.py +226 -0
- coretrace_python/abstract/heap.py +252 -0
- coretrace_python/abstract/ranges.py +285 -0
- coretrace_python/abstract/values.py +56 -0
- coretrace_python/analysis/__init__.py +31 -0
- coretrace_python/analysis/manager.py +165 -0
- coretrace_python/analysis/provider.py +73 -0
- coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
- coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
- coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
- coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
- coretrace_python/bundled/models/cli/cli_models.py +35 -0
- coretrace_python/bundled/models/cli/plugin.toml +9 -0
- coretrace_python/bundled/models/credentials/credential_models.py +43 -0
- coretrace_python/bundled/models/credentials/plugin.toml +9 -0
- coretrace_python/bundled/models/django/django_models.py +123 -0
- coretrace_python/bundled/models/django/plugin.toml +9 -0
- coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
- coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
- coretrace_python/bundled/models/flask/flask_models.py +53 -0
- coretrace_python/bundled/models/flask/plugin.toml +9 -0
- coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
- coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
- coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
- coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
- coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
- coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
- coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
- coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
- coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
- coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
- coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
- coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
- coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
- coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
- coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
- coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
- coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
- coretrace_python/bundled/security/xss/plugin.toml +9 -0
- coretrace_python/bundled/security/xss/xss.py +17 -0
- coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
- coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
- coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
- coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
- coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
- coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
- coretrace_python/cache.py +310 -0
- coretrace_python/cfg/__init__.py +46 -0
- coretrace_python/cfg/builder.py +589 -0
- coretrace_python/cfg/dominance.py +183 -0
- coretrace_python/cfg/model.py +166 -0
- coretrace_python/cli.py +216 -0
- coretrace_python/dataflow/__init__.py +29 -0
- coretrace_python/dataflow/lattice.py +78 -0
- coretrace_python/dataflow/solver.py +96 -0
- coretrace_python/dependency/__init__.py +44 -0
- coretrace_python/dependency/advisories.py +168 -0
- coretrace_python/dependency/correlation.py +87 -0
- coretrace_python/dependency/graph.py +274 -0
- coretrace_python/dependency/policy.py +65 -0
- coretrace_python/dependency/sbom.py +65 -0
- coretrace_python/engine.py +688 -0
- coretrace_python/findings/__init__.py +13 -0
- coretrace_python/findings/coverage.py +45 -0
- coretrace_python/findings/model.py +49 -0
- coretrace_python/findings/refutation.py +427 -0
- coretrace_python/frontend/__init__.py +18 -0
- coretrace_python/frontend/ast_adapter.py +447 -0
- coretrace_python/frontend/parser.py +23 -0
- coretrace_python/hir/__init__.py +5 -0
- coretrace_python/hir/nodes.py +584 -0
- coretrace_python/hir/visitors.py +36 -0
- coretrace_python/interprocedural/__init__.py +49 -0
- coretrace_python/interprocedural/callgraph.py +291 -0
- coretrace_python/interprocedural/modulegraph.py +218 -0
- coretrace_python/interprocedural/summaries.py +463 -0
- coretrace_python/ir/__init__.py +5 -0
- coretrace_python/ir/defuse.py +74 -0
- coretrace_python/ir/lowering.py +575 -0
- coretrace_python/ir/model.py +481 -0
- coretrace_python/ir/printer.py +201 -0
- coretrace_python/ir/ssa.py +277 -0
- coretrace_python/plugins/__init__.py +58 -0
- coretrace_python/plugins/api.py +153 -0
- coretrace_python/plugins/detectors.py +114 -0
- coretrace_python/plugins/loader.py +84 -0
- coretrace_python/plugins/manifest.py +112 -0
- coretrace_python/plugins/registry.py +34 -0
- coretrace_python/plugins/secrets.py +330 -0
- coretrace_python/reporters/__init__.py +22 -0
- coretrace_python/reporters/json_format.py +48 -0
- coretrace_python/reporters/report.py +23 -0
- coretrace_python/reporters/sarif.py +70 -0
- coretrace_python/reporters/text.py +24 -0
- coretrace_python/semantic/__init__.py +9 -0
- coretrace_python/semantic/identity.py +39 -0
- coretrace_python/semantic/imports.py +131 -0
- coretrace_python/semantic/scopes.py +473 -0
- coretrace_python/semantic/symbols.py +87 -0
- coretrace_python/source/__init__.py +13 -0
- coretrace_python/source/manager.py +81 -0
- coretrace_python/source/model.py +57 -0
- coretrace_python/taint/__init__.py +55 -0
- coretrace_python/taint/engine.py +800 -0
- coretrace_python/taint/models.py +317 -0
- coretrace_python/taint/routes.py +99 -0
- coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
- coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
- coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
- coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Abstract values and the analyses that compute them (architecture §18)."""
|
|
2
|
+
|
|
3
|
+
from coretrace_python.abstract.constants import (
|
|
4
|
+
ConstantFacts,
|
|
5
|
+
ConstantPropagation,
|
|
6
|
+
propagate_constants,
|
|
7
|
+
)
|
|
8
|
+
from coretrace_python.abstract.heap import (
|
|
9
|
+
ATTRIBUTES,
|
|
10
|
+
ELEMENTS,
|
|
11
|
+
MUTATORS,
|
|
12
|
+
AbstractObject,
|
|
13
|
+
AliasSet,
|
|
14
|
+
AllocationSite,
|
|
15
|
+
HeapAnalysis,
|
|
16
|
+
HeapFacts,
|
|
17
|
+
HeapLocation,
|
|
18
|
+
analyze_heap,
|
|
19
|
+
mutated_by,
|
|
20
|
+
)
|
|
21
|
+
from coretrace_python.abstract.ranges import Interval, RangeAnalysis, RangeFacts, analyze_ranges
|
|
22
|
+
from coretrace_python.abstract.values import AbstractValue, Truth
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ATTRIBUTES",
|
|
26
|
+
"ELEMENTS",
|
|
27
|
+
"MUTATORS",
|
|
28
|
+
"AbstractObject",
|
|
29
|
+
"AbstractValue",
|
|
30
|
+
"AliasSet",
|
|
31
|
+
"AllocationSite",
|
|
32
|
+
"ConstantFacts",
|
|
33
|
+
"ConstantPropagation",
|
|
34
|
+
"HeapAnalysis",
|
|
35
|
+
"HeapFacts",
|
|
36
|
+
"HeapLocation",
|
|
37
|
+
"Interval",
|
|
38
|
+
"RangeAnalysis",
|
|
39
|
+
"RangeFacts",
|
|
40
|
+
"Truth",
|
|
41
|
+
"analyze_heap",
|
|
42
|
+
"analyze_ranges",
|
|
43
|
+
"mutated_by",
|
|
44
|
+
"propagate_constants",
|
|
45
|
+
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Constant propagation: the reference client of the data-flow solver (§18, §38)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import operator
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
from typing import Any, ClassVar
|
|
9
|
+
|
|
10
|
+
from coretrace_python.abstract.values import AbstractValue, Truth
|
|
11
|
+
from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
|
|
12
|
+
from coretrace_python.cfg import CFG, BlockId, CFGAnalysis
|
|
13
|
+
from coretrace_python.dataflow import BOTTOM, TOP, DataflowProblem, Direction, solve
|
|
14
|
+
from coretrace_python.hir import nodes
|
|
15
|
+
from coretrace_python.ir.model import (
|
|
16
|
+
BasicBlock,
|
|
17
|
+
BinaryOp,
|
|
18
|
+
Branch,
|
|
19
|
+
Compare,
|
|
20
|
+
Constant,
|
|
21
|
+
ForNext,
|
|
22
|
+
FunctionIR,
|
|
23
|
+
Instruction,
|
|
24
|
+
Jump,
|
|
25
|
+
Phi,
|
|
26
|
+
UnaryOp,
|
|
27
|
+
Value,
|
|
28
|
+
)
|
|
29
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
30
|
+
|
|
31
|
+
State = Mapping[Value, AbstractValue]
|
|
32
|
+
|
|
33
|
+
_SAFE_TYPES = (int, float, bool, str, bytes, type(None))
|
|
34
|
+
_NUMERIC = frozenset({"int", "float", "bool"})
|
|
35
|
+
|
|
36
|
+
_BINARY: Mapping[str, Callable[..., Any]] = {
|
|
37
|
+
"add": operator.add,
|
|
38
|
+
"sub": operator.sub,
|
|
39
|
+
"mul": operator.mul,
|
|
40
|
+
"div": operator.truediv,
|
|
41
|
+
"floor_div": operator.floordiv,
|
|
42
|
+
"mod": operator.mod,
|
|
43
|
+
"bit_or": operator.or_,
|
|
44
|
+
"bit_xor": operator.xor,
|
|
45
|
+
"bit_and": operator.and_,
|
|
46
|
+
}
|
|
47
|
+
_COMPARE: Mapping[str, Callable[..., Any]] = {
|
|
48
|
+
"eq": operator.eq,
|
|
49
|
+
"not_eq": operator.ne,
|
|
50
|
+
"lt": operator.lt,
|
|
51
|
+
"lt_eq": operator.le,
|
|
52
|
+
"gt": operator.gt,
|
|
53
|
+
"gt_eq": operator.ge,
|
|
54
|
+
"is": operator.is_,
|
|
55
|
+
"is_not": operator.is_not,
|
|
56
|
+
"in": lambda a, b: a in b,
|
|
57
|
+
"not_in": lambda a, b: a not in b,
|
|
58
|
+
}
|
|
59
|
+
_UNARY: Mapping[str, Callable[..., Any]] = {
|
|
60
|
+
"neg": operator.neg,
|
|
61
|
+
"pos": operator.pos,
|
|
62
|
+
"invert": operator.invert,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ConstantFacts:
|
|
67
|
+
def __init__(self, values: Mapping[Value, AbstractValue], reachable: frozenset[BlockId]):
|
|
68
|
+
self._values = MappingProxyType(dict(values))
|
|
69
|
+
self._reachable = reachable
|
|
70
|
+
|
|
71
|
+
def value(self, value: Value) -> AbstractValue:
|
|
72
|
+
return self._values.get(value, AbstractValue.bottom())
|
|
73
|
+
|
|
74
|
+
def reachable(self, block: BlockId) -> bool:
|
|
75
|
+
return block in self._reachable
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _ConstantProblem(DataflowProblem[State]):
|
|
79
|
+
direction: ClassVar[Direction] = Direction.FORWARD
|
|
80
|
+
|
|
81
|
+
def __init__(self, function: FunctionIR) -> None:
|
|
82
|
+
self.function = function
|
|
83
|
+
self.blocks = {block.id: block for block in function.blocks}
|
|
84
|
+
|
|
85
|
+
def initial(self) -> State:
|
|
86
|
+
return MappingProxyType({p: AbstractValue.unknown() for p in self.function.parameters})
|
|
87
|
+
|
|
88
|
+
def join(self, a: State, b: State) -> State:
|
|
89
|
+
merged = dict(a)
|
|
90
|
+
for value, fact in b.items():
|
|
91
|
+
merged[value] = merged[value].join(fact) if value in merged else fact
|
|
92
|
+
return MappingProxyType(merged)
|
|
93
|
+
|
|
94
|
+
def evaluate(self, block: BasicBlock, incoming: Mapping[BlockId, State]) -> State:
|
|
95
|
+
"""State after ``block`` given the states on its executable incoming edges."""
|
|
96
|
+
|
|
97
|
+
states = list(incoming.values())
|
|
98
|
+
state = dict(states[0])
|
|
99
|
+
for other in states[1:]:
|
|
100
|
+
state = dict(self.join(state, other))
|
|
101
|
+
for instruction in block.instructions:
|
|
102
|
+
if instruction.result is None:
|
|
103
|
+
continue
|
|
104
|
+
state[instruction.result] = (
|
|
105
|
+
self.phi(instruction, incoming)
|
|
106
|
+
if isinstance(instruction, Phi)
|
|
107
|
+
else self.instruction(instruction, state)
|
|
108
|
+
)
|
|
109
|
+
if isinstance(block.terminator, ForNext) and block.terminator.result is not None:
|
|
110
|
+
state[block.terminator.result] = AbstractValue.unknown()
|
|
111
|
+
return MappingProxyType(state)
|
|
112
|
+
|
|
113
|
+
def flow(self, cfg: CFG, block_id: BlockId, incoming: Mapping[BlockId, State]) -> Mapping[BlockId, State]:
|
|
114
|
+
block = self.blocks[block_id]
|
|
115
|
+
state = self.evaluate(block, incoming)
|
|
116
|
+
exits = {target: state for target in block.exception_targets}
|
|
117
|
+
terminator = block.terminator
|
|
118
|
+
if isinstance(terminator, Branch):
|
|
119
|
+
truth = state[terminator.condition].truthiness
|
|
120
|
+
targets = {
|
|
121
|
+
Truth.TRUE: (terminator.then_block,),
|
|
122
|
+
Truth.FALSE: (terminator.else_block,),
|
|
123
|
+
Truth.UNKNOWN: (terminator.then_block, terminator.else_block),
|
|
124
|
+
}[truth]
|
|
125
|
+
return {**exits, **{target: state for target in targets}}
|
|
126
|
+
if isinstance(terminator, Jump):
|
|
127
|
+
return {**exits, terminator.target: state}
|
|
128
|
+
if isinstance(terminator, ForNext):
|
|
129
|
+
return {**exits, terminator.body: state, terminator.exit: state}
|
|
130
|
+
return exits
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------ transfer
|
|
133
|
+
|
|
134
|
+
def phi(self, phi: Phi, incoming: Mapping[BlockId, State]) -> AbstractValue:
|
|
135
|
+
result = AbstractValue.bottom()
|
|
136
|
+
for value, predecessor in phi.incoming:
|
|
137
|
+
if predecessor in incoming:
|
|
138
|
+
result = result.join(incoming[predecessor].get(value, AbstractValue.unknown()))
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
def instruction(self, instruction: Instruction, state: Mapping[Value, AbstractValue]) -> AbstractValue:
|
|
142
|
+
if isinstance(instruction, Constant):
|
|
143
|
+
return AbstractValue.of(instruction.value)
|
|
144
|
+
if isinstance(instruction, BinaryOp):
|
|
145
|
+
return self.binary(instruction, state[instruction.left], state[instruction.right])
|
|
146
|
+
if isinstance(instruction, Compare):
|
|
147
|
+
return self.fold(_COMPARE[instruction.operator], state[instruction.left], state[instruction.right])
|
|
148
|
+
if isinstance(instruction, UnaryOp):
|
|
149
|
+
return self.unary(instruction, state[instruction.operand])
|
|
150
|
+
return AbstractValue.unknown()
|
|
151
|
+
|
|
152
|
+
def binary(self, op: BinaryOp, left: AbstractValue, right: AbstractValue) -> AbstractValue:
|
|
153
|
+
folder = _BINARY.get(op.operator)
|
|
154
|
+
if folder is not None:
|
|
155
|
+
numeric_only = op.operator == "mul"
|
|
156
|
+
folded = self.fold(folder, left, right, numeric_only=numeric_only)
|
|
157
|
+
if folded.constant is not TOP:
|
|
158
|
+
return folded
|
|
159
|
+
return AbstractValue.unknown(self.result_types(op.operator, left, right))
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def result_types(operator_name: str, left: AbstractValue, right: AbstractValue) -> frozenset[str] | None:
|
|
163
|
+
if left.types is None or right.types is None:
|
|
164
|
+
return None
|
|
165
|
+
if left.types <= _NUMERIC and right.types <= _NUMERIC:
|
|
166
|
+
if operator_name == "div" or "float" in left.types | right.types:
|
|
167
|
+
return frozenset({"float"})
|
|
168
|
+
if operator_name in _BINARY:
|
|
169
|
+
return frozenset({"int"})
|
|
170
|
+
if operator_name == "add" and left.types == right.types == frozenset({"str"}):
|
|
171
|
+
return frozenset({"str"})
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def unary(self, op: UnaryOp, operand: AbstractValue) -> AbstractValue:
|
|
175
|
+
if op.operator == "not":
|
|
176
|
+
if operand.truthiness is Truth.UNKNOWN:
|
|
177
|
+
return AbstractValue.unknown(frozenset({"bool"}))
|
|
178
|
+
return AbstractValue.of(operand.truthiness is Truth.FALSE)
|
|
179
|
+
folder = _UNARY.get(op.operator)
|
|
180
|
+
if folder is None or operand.constant is TOP or operand.constant is BOTTOM:
|
|
181
|
+
return AbstractValue.unknown()
|
|
182
|
+
if not isinstance(operand.constant, (int, float, bool)):
|
|
183
|
+
return AbstractValue.unknown()
|
|
184
|
+
try:
|
|
185
|
+
return AbstractValue.of(folder(operand.constant))
|
|
186
|
+
except (TypeError, ValueError, ArithmeticError):
|
|
187
|
+
return AbstractValue.unknown()
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def fold(
|
|
191
|
+
folder: Callable[..., Any],
|
|
192
|
+
left: AbstractValue,
|
|
193
|
+
right: AbstractValue,
|
|
194
|
+
*,
|
|
195
|
+
numeric_only: bool = False,
|
|
196
|
+
) -> AbstractValue:
|
|
197
|
+
for side in (left.constant, right.constant):
|
|
198
|
+
if side is TOP or side is BOTTOM or not isinstance(side, _SAFE_TYPES):
|
|
199
|
+
return AbstractValue.unknown()
|
|
200
|
+
if numeric_only and not all(isinstance(c, (int, float, bool)) for c in (left.constant, right.constant)):
|
|
201
|
+
return AbstractValue.unknown()
|
|
202
|
+
try:
|
|
203
|
+
return AbstractValue.of(folder(left.constant, right.constant))
|
|
204
|
+
except (TypeError, ValueError, ArithmeticError):
|
|
205
|
+
return AbstractValue.unknown()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def propagate_constants(function: FunctionIR, cfg: CFG) -> ConstantFacts:
|
|
209
|
+
problem = _ConstantProblem(function)
|
|
210
|
+
solution = solve(problem, cfg)
|
|
211
|
+
values: dict[Value, AbstractValue] = {}
|
|
212
|
+
reachable: set[BlockId] = set()
|
|
213
|
+
for block in function.blocks:
|
|
214
|
+
if solution.reached(block.id):
|
|
215
|
+
reachable.add(block.id)
|
|
216
|
+
values.update(problem.evaluate(block, solution.incoming(block.id)))
|
|
217
|
+
return ConstantFacts(values, frozenset(reachable))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class ConstantPropagation(FunctionAnalysis[ConstantFacts]):
|
|
221
|
+
name: ClassVar[str] = "abstract.constants"
|
|
222
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CFGAnalysis})
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> ConstantFacts:
|
|
226
|
+
return propagate_constants(ctx.get(SSAAnalysis, function), ctx.get(CFGAnalysis, function))
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Coarse heap and aliasing abstraction (architecture §22).
|
|
2
|
+
|
|
3
|
+
SSA names values, not objects. This domain gives every allocation site one
|
|
4
|
+
``AbstractObject``: containers and calls at their instruction, parameters, module
|
|
5
|
+
globals and imported symbols by name, and the fields loaded from an object by field.
|
|
6
|
+
Each value points to an ``AliasSet`` of objects, computed by a flow-insensitive
|
|
7
|
+
points-to fixpoint over the SSA form; each object has two ``HeapLocation`` fields,
|
|
8
|
+
``elements`` for its items and ``attributes`` for its attributes, field-insensitive
|
|
9
|
+
within each. Taint and dependence analyses key their states by these locations, so a
|
|
10
|
+
store, a mutating method call or a load on any alias reads and writes the same place.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from types import MappingProxyType
|
|
18
|
+
from typing import ClassVar
|
|
19
|
+
|
|
20
|
+
from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
|
|
21
|
+
from coretrace_python.hir import nodes
|
|
22
|
+
from coretrace_python.ir.model import (
|
|
23
|
+
Await,
|
|
24
|
+
BuildDict,
|
|
25
|
+
BuildList,
|
|
26
|
+
BuildSet,
|
|
27
|
+
BuildTuple,
|
|
28
|
+
Call,
|
|
29
|
+
Catch,
|
|
30
|
+
ForNext,
|
|
31
|
+
FunctionIR,
|
|
32
|
+
GetAttr,
|
|
33
|
+
GetItem,
|
|
34
|
+
GetIter,
|
|
35
|
+
Global,
|
|
36
|
+
Instruction,
|
|
37
|
+
Phi,
|
|
38
|
+
SetAttr,
|
|
39
|
+
SetItem,
|
|
40
|
+
Symbol,
|
|
41
|
+
Value,
|
|
42
|
+
WithEnter,
|
|
43
|
+
Yield,
|
|
44
|
+
)
|
|
45
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
46
|
+
from coretrace_python.source import SourceSpan
|
|
47
|
+
|
|
48
|
+
ELEMENTS = "elements"
|
|
49
|
+
ATTRIBUTES = "attributes"
|
|
50
|
+
|
|
51
|
+
# Method names that store their arguments into the receiver's elements.
|
|
52
|
+
MUTATORS = frozenset(
|
|
53
|
+
{"append", "appendleft", "extend", "extendleft", "insert", "add", "update", "setdefault", "put"}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class AllocationSite:
|
|
59
|
+
kind: str
|
|
60
|
+
location: SourceSpan
|
|
61
|
+
name: str = ""
|
|
62
|
+
ordinal: int = 0
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
text = f"{self.kind}@{self.location.source_id}:{self.location.start_line}"
|
|
66
|
+
if self.name:
|
|
67
|
+
text += f":{self.name}"
|
|
68
|
+
if self.ordinal:
|
|
69
|
+
text += f"#{self.ordinal}"
|
|
70
|
+
return text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class AbstractObject:
|
|
75
|
+
site: AllocationSite
|
|
76
|
+
|
|
77
|
+
def __str__(self) -> str:
|
|
78
|
+
return str(self.site)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class HeapLocation:
|
|
83
|
+
object: AbstractObject
|
|
84
|
+
field: str
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
return f"{self.object}.{self.field}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
AliasSet = frozenset[AbstractObject]
|
|
91
|
+
NOTHING: AliasSet = frozenset()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class HeapFacts:
|
|
95
|
+
def __init__(self, objects: Mapping[Value, AliasSet]) -> None:
|
|
96
|
+
self._objects: Mapping[Value, AliasSet] = MappingProxyType(dict(objects))
|
|
97
|
+
self.values = tuple(objects)
|
|
98
|
+
|
|
99
|
+
def objects(self, value: Value) -> AliasSet:
|
|
100
|
+
return self._objects.get(value, NOTHING)
|
|
101
|
+
|
|
102
|
+
def locations(self, value: Value, field: str) -> tuple[HeapLocation, ...]:
|
|
103
|
+
return tuple(HeapLocation(o, field) for o in sorted(self.objects(value), key=str))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def mutated_by(call: Call, defs: Mapping[Value, Instruction]) -> Value | None:
|
|
107
|
+
"""The receiver of a call to a mutating method (``xs.append(v)``), if any."""
|
|
108
|
+
|
|
109
|
+
callee = defs.get(call.callee)
|
|
110
|
+
if isinstance(callee, GetAttr) and callee.attribute in MUTATORS:
|
|
111
|
+
return callee.object
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class _PointsTo:
|
|
116
|
+
def __init__(self, function: FunctionIR) -> None:
|
|
117
|
+
self.function = function
|
|
118
|
+
self.defs: dict[Value, Instruction] = {
|
|
119
|
+
i.result: i for block in function.blocks for i in block.instructions if i.result
|
|
120
|
+
}
|
|
121
|
+
self.points: dict[Value, set[AbstractObject]] = {}
|
|
122
|
+
self.heap: dict[HeapLocation, set[AbstractObject]] = {}
|
|
123
|
+
self.named: dict[tuple[str, str], AbstractObject] = {}
|
|
124
|
+
for index, parameter in enumerate(function.parameters):
|
|
125
|
+
self.points[parameter] = {
|
|
126
|
+
AbstractObject(AllocationSite("parameter", function.location, ordinal=index))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def named_object(self, kind: str, name: str) -> AbstractObject:
|
|
130
|
+
key = (kind, name)
|
|
131
|
+
if key not in self.named:
|
|
132
|
+
self.named[key] = AbstractObject(AllocationSite(kind, self.function.location, name))
|
|
133
|
+
return self.named[key]
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def field_object(owner: AbstractObject, field: str) -> AbstractObject:
|
|
137
|
+
# A field of a field collapses onto itself, so chains such as ``node.next``
|
|
138
|
+
# in a loop stay finite.
|
|
139
|
+
if owner.site.kind == "field":
|
|
140
|
+
return owner
|
|
141
|
+
return AbstractObject(AllocationSite("field", owner.site.location, f"{owner.site}.{field}"))
|
|
142
|
+
|
|
143
|
+
def of(self, value: Value) -> set[AbstractObject]:
|
|
144
|
+
return self.points.setdefault(value, set())
|
|
145
|
+
|
|
146
|
+
def at(self, objects: set[AbstractObject], field: str) -> set[AbstractObject]:
|
|
147
|
+
found: set[AbstractObject] = set()
|
|
148
|
+
for owner in objects:
|
|
149
|
+
found |= self.heap.setdefault(HeapLocation(owner, field), set())
|
|
150
|
+
found.add(self.field_object(owner, field))
|
|
151
|
+
return found
|
|
152
|
+
|
|
153
|
+
def store(self, objects: set[AbstractObject], field: str, value: Value) -> bool:
|
|
154
|
+
changed = False
|
|
155
|
+
for owner in objects:
|
|
156
|
+
location = self.heap.setdefault(HeapLocation(owner, field), set())
|
|
157
|
+
before = len(location)
|
|
158
|
+
location |= self.of(value)
|
|
159
|
+
changed |= len(location) != before
|
|
160
|
+
return changed
|
|
161
|
+
|
|
162
|
+
def solve(self) -> None:
|
|
163
|
+
changed = True
|
|
164
|
+
while changed:
|
|
165
|
+
changed = False
|
|
166
|
+
for block in self.function.blocks:
|
|
167
|
+
for instruction in block.instructions:
|
|
168
|
+
changed |= self.instruction(instruction)
|
|
169
|
+
terminator = block.terminator
|
|
170
|
+
if isinstance(terminator, ForNext) and terminator.result is not None:
|
|
171
|
+
changed |= self.assign(terminator.result, self.at(self.of(terminator.iterator), ELEMENTS))
|
|
172
|
+
|
|
173
|
+
def assign(self, value: Value, objects: set[AbstractObject]) -> bool:
|
|
174
|
+
current = self.of(value)
|
|
175
|
+
before = len(current)
|
|
176
|
+
current |= objects
|
|
177
|
+
return len(current) != before
|
|
178
|
+
|
|
179
|
+
def instruction(self, instruction: Instruction) -> bool:
|
|
180
|
+
if isinstance(instruction, SetAttr):
|
|
181
|
+
return self.store(self.of(instruction.object), ATTRIBUTES, instruction.value)
|
|
182
|
+
if isinstance(instruction, SetItem):
|
|
183
|
+
return self.store(self.of(instruction.object), ELEMENTS, instruction.value)
|
|
184
|
+
result = instruction.result
|
|
185
|
+
if result is None:
|
|
186
|
+
return False
|
|
187
|
+
if isinstance(instruction, BuildList | BuildTuple | BuildDict | BuildSet | Call | WithEnter | Catch | Yield):
|
|
188
|
+
kind = {
|
|
189
|
+
BuildList: "list",
|
|
190
|
+
BuildTuple: "tuple",
|
|
191
|
+
BuildDict: "dict",
|
|
192
|
+
BuildSet: "set",
|
|
193
|
+
Call: "call",
|
|
194
|
+
WithEnter: "context",
|
|
195
|
+
Catch: "exception",
|
|
196
|
+
Yield: "sent",
|
|
197
|
+
}[type(instruction)]
|
|
198
|
+
site = AbstractObject(AllocationSite(kind, instruction.location))
|
|
199
|
+
changed = self.assign(result, {site})
|
|
200
|
+
if isinstance(instruction, BuildList | BuildTuple | BuildDict | BuildSet):
|
|
201
|
+
values = (
|
|
202
|
+
tuple(v for _, v in instruction.items)
|
|
203
|
+
if isinstance(instruction, BuildDict)
|
|
204
|
+
else instruction.elements
|
|
205
|
+
)
|
|
206
|
+
for element in values:
|
|
207
|
+
changed |= self.store({site}, ELEMENTS, element)
|
|
208
|
+
for unpacked in instruction.unpacked:
|
|
209
|
+
contents = self.at(self.of(unpacked), ELEMENTS)
|
|
210
|
+
location = self.heap.setdefault(HeapLocation(site, ELEMENTS), set())
|
|
211
|
+
before = len(location)
|
|
212
|
+
location |= contents
|
|
213
|
+
changed |= len(location) != before
|
|
214
|
+
elif isinstance(instruction, Call):
|
|
215
|
+
receiver = mutated_by(instruction, self.defs)
|
|
216
|
+
if receiver is not None:
|
|
217
|
+
for argument in instruction.argument_values():
|
|
218
|
+
changed |= self.store(self.of(receiver), ELEMENTS, argument)
|
|
219
|
+
return changed
|
|
220
|
+
if isinstance(instruction, Global):
|
|
221
|
+
return self.assign(result, {self.named_object("global", instruction.name)})
|
|
222
|
+
if isinstance(instruction, Symbol):
|
|
223
|
+
return self.assign(result, {self.named_object("symbol", instruction.symbol_id.canonical_name)})
|
|
224
|
+
if isinstance(instruction, Phi):
|
|
225
|
+
objects: set[AbstractObject] = set()
|
|
226
|
+
for value, _ in instruction.incoming:
|
|
227
|
+
objects |= self.of(value)
|
|
228
|
+
return self.assign(result, objects)
|
|
229
|
+
if isinstance(instruction, GetAttr):
|
|
230
|
+
return self.assign(result, self.at(self.of(instruction.object), ATTRIBUTES))
|
|
231
|
+
if isinstance(instruction, GetItem):
|
|
232
|
+
return self.assign(result, self.at(self.of(instruction.object), ELEMENTS))
|
|
233
|
+
if isinstance(instruction, GetIter):
|
|
234
|
+
return self.assign(result, self.of(instruction.iterable))
|
|
235
|
+
if isinstance(instruction, Await):
|
|
236
|
+
return self.assign(result, self.of(instruction.value))
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def analyze_heap(function: FunctionIR) -> HeapFacts:
|
|
241
|
+
solver = _PointsTo(function)
|
|
242
|
+
solver.solve()
|
|
243
|
+
return HeapFacts({value: frozenset(objects) for value, objects in solver.points.items() if objects})
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class HeapAnalysis(FunctionAnalysis[HeapFacts]):
|
|
247
|
+
name: ClassVar[str] = "abstract.heap"
|
|
248
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis})
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> HeapFacts:
|
|
252
|
+
return analyze_heap(ctx.get(SSAAnalysis, function))
|