crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.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.
- _crosshair_tracers.cpython-312-darwin.so +0 -0
- crosshair/__init__.py +42 -0
- crosshair/__main__.py +8 -0
- crosshair/_mark_stacks.h +790 -0
- crosshair/_preliminaries_test.py +18 -0
- crosshair/_tracers.h +94 -0
- crosshair/_tracers_pycompat.h +522 -0
- crosshair/_tracers_test.py +138 -0
- crosshair/abcstring.py +245 -0
- crosshair/auditwall.py +190 -0
- crosshair/auditwall_test.py +77 -0
- crosshair/codeconfig.py +113 -0
- crosshair/codeconfig_test.py +117 -0
- crosshair/condition_parser.py +1237 -0
- crosshair/condition_parser_test.py +497 -0
- crosshair/conftest.py +30 -0
- crosshair/copyext.py +155 -0
- crosshair/copyext_test.py +84 -0
- crosshair/core.py +1763 -0
- crosshair/core_and_libs.py +149 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +1316 -0
- crosshair/diff_behavior.py +314 -0
- crosshair/diff_behavior_test.py +261 -0
- crosshair/dynamic_typing.py +346 -0
- crosshair/dynamic_typing_test.py +210 -0
- crosshair/enforce.py +282 -0
- crosshair/enforce_test.py +182 -0
- crosshair/examples/PEP316/__init__.py +1 -0
- crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
- crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
- crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
- crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
- crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
- crosshair/examples/PEP316/correct_code/__init__.py +0 -0
- crosshair/examples/PEP316/correct_code/arith.py +60 -0
- crosshair/examples/PEP316/correct_code/chess.py +77 -0
- crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
- crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
- crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
- crosshair/examples/PEP316/correct_code/showcase.py +104 -0
- crosshair/examples/__init__.py +0 -0
- crosshair/examples/check_examples_test.py +146 -0
- crosshair/examples/deal/__init__.py +1 -0
- crosshair/examples/icontract/__init__.py +1 -0
- crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
- crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
- crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
- crosshair/examples/icontract/correct_code/__init__.py +0 -0
- crosshair/examples/icontract/correct_code/arith.py +51 -0
- crosshair/examples/icontract/correct_code/showcase.py +94 -0
- crosshair/fnutil.py +391 -0
- crosshair/fnutil_test.py +75 -0
- crosshair/fuzz_core_test.py +516 -0
- crosshair/libimpl/__init__.py +0 -0
- crosshair/libimpl/arraylib.py +161 -0
- crosshair/libimpl/binascii_ch_test.py +30 -0
- crosshair/libimpl/binascii_test.py +67 -0
- crosshair/libimpl/binasciilib.py +150 -0
- crosshair/libimpl/bisectlib_test.py +23 -0
- crosshair/libimpl/builtinslib.py +5228 -0
- crosshair/libimpl/builtinslib_ch_test.py +1191 -0
- crosshair/libimpl/builtinslib_test.py +3735 -0
- crosshair/libimpl/codecslib.py +86 -0
- crosshair/libimpl/codecslib_test.py +86 -0
- crosshair/libimpl/collectionslib.py +264 -0
- crosshair/libimpl/collectionslib_ch_test.py +252 -0
- crosshair/libimpl/collectionslib_test.py +332 -0
- crosshair/libimpl/copylib.py +23 -0
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +2559 -0
- crosshair/libimpl/datetimelib_ch_test.py +354 -0
- crosshair/libimpl/datetimelib_test.py +112 -0
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/__init__.py +23 -0
- crosshair/libimpl/encodings/_encutil.py +187 -0
- crosshair/libimpl/encodings/ascii.py +44 -0
- crosshair/libimpl/encodings/latin_1.py +40 -0
- crosshair/libimpl/encodings/utf_8.py +93 -0
- crosshair/libimpl/encodings_ch_test.py +83 -0
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +34 -0
- crosshair/libimpl/functoolslib_test.py +56 -0
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +47 -0
- crosshair/libimpl/heapqlib_test.py +21 -0
- crosshair/libimpl/importliblib.py +18 -0
- crosshair/libimpl/importliblib_test.py +38 -0
- crosshair/libimpl/iolib.py +216 -0
- crosshair/libimpl/iolib_ch_test.py +128 -0
- crosshair/libimpl/iolib_test.py +19 -0
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib.py +44 -0
- crosshair/libimpl/itertoolslib_test.py +44 -0
- crosshair/libimpl/jsonlib.py +984 -0
- crosshair/libimpl/jsonlib_ch_test.py +42 -0
- crosshair/libimpl/jsonlib_test.py +51 -0
- crosshair/libimpl/mathlib.py +179 -0
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +67 -0
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +178 -0
- crosshair/libimpl/randomlib_test.py +120 -0
- crosshair/libimpl/relib.py +846 -0
- crosshair/libimpl/relib_ch_test.py +169 -0
- crosshair/libimpl/relib_test.py +493 -0
- crosshair/libimpl/timelib.py +72 -0
- crosshair/libimpl/timelib_test.py +82 -0
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib.py +75 -0
- crosshair/libimpl/unicodedatalib_test.py +42 -0
- crosshair/libimpl/urlliblib.py +23 -0
- crosshair/libimpl/urlliblib_test.py +19 -0
- crosshair/libimpl/weakreflib.py +13 -0
- crosshair/libimpl/weakreflib_test.py +69 -0
- crosshair/libimpl/zliblib.py +15 -0
- crosshair/libimpl/zliblib_test.py +13 -0
- crosshair/lsp_server.py +261 -0
- crosshair/lsp_server_test.py +30 -0
- crosshair/main.py +973 -0
- crosshair/main_test.py +543 -0
- crosshair/objectproxy.py +376 -0
- crosshair/objectproxy_test.py +41 -0
- crosshair/opcode_intercept.py +601 -0
- crosshair/opcode_intercept_test.py +304 -0
- crosshair/options.py +218 -0
- crosshair/options_test.py +10 -0
- crosshair/patch_equivalence_test.py +75 -0
- crosshair/path_cover.py +209 -0
- crosshair/path_cover_test.py +138 -0
- crosshair/path_search.py +161 -0
- crosshair/path_search_test.py +52 -0
- crosshair/pathing_oracle.py +271 -0
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer.py +27 -0
- crosshair/pure_importer_test.py +16 -0
- crosshair/py.typed +0 -0
- crosshair/register_contract.py +273 -0
- crosshair/register_contract_test.py +190 -0
- crosshair/simplestructs.py +1165 -0
- crosshair/simplestructs_test.py +283 -0
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +1199 -0
- crosshair/statespace_test.py +108 -0
- crosshair/stubs_parser.py +352 -0
- crosshair/stubs_parser_test.py +43 -0
- crosshair/test_util.py +329 -0
- crosshair/test_util_test.py +26 -0
- crosshair/tools/__init__.py +0 -0
- crosshair/tools/check_help_in_doc.py +264 -0
- crosshair/tools/check_init_and_setup_coincide.py +119 -0
- crosshair/tools/generate_demo_table.py +127 -0
- crosshair/tracers.py +544 -0
- crosshair/tracers_test.py +154 -0
- crosshair/type_repo.py +151 -0
- crosshair/unicode_categories.py +589 -0
- crosshair/unicode_categories_test.py +27 -0
- crosshair/util.py +741 -0
- crosshair/util_test.py +173 -0
- crosshair/watcher.py +307 -0
- crosshair/watcher_test.py +107 -0
- crosshair/z3util.py +76 -0
- crosshair/z3util_test.py +11 -0
- crosshair_tool-0.0.99.dist-info/METADATA +144 -0
- crosshair_tool-0.0.99.dist-info/RECORD +176 -0
- crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
- crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
- crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
- crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import dis
|
|
2
|
+
import sys
|
|
3
|
+
import weakref
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from collections.abc import MutableMapping, Set
|
|
6
|
+
from sys import version_info
|
|
7
|
+
from types import CodeType, FrameType
|
|
8
|
+
from typing import Any, Callable, Iterable, List, Mapping, Tuple, Union
|
|
9
|
+
|
|
10
|
+
from z3 import ExprRef # type: ignore
|
|
11
|
+
|
|
12
|
+
from crosshair.core import (
|
|
13
|
+
ATOMIC_IMMUTABLE_TYPES,
|
|
14
|
+
register_opcode_patch,
|
|
15
|
+
with_uniform_probabilities,
|
|
16
|
+
)
|
|
17
|
+
from crosshair.libimpl.builtinslib import (
|
|
18
|
+
AnySymbolicStr,
|
|
19
|
+
AtomicSymbolicValue,
|
|
20
|
+
ModelingDirector,
|
|
21
|
+
SymbolicBool,
|
|
22
|
+
SymbolicInt,
|
|
23
|
+
SymbolicList,
|
|
24
|
+
python_types_using_atomic_symbolics,
|
|
25
|
+
)
|
|
26
|
+
from crosshair.simplestructs import LinearSet, ShellMutableSet, SimpleDict, SliceView
|
|
27
|
+
from crosshair.statespace import context_statespace
|
|
28
|
+
from crosshair.tracers import (
|
|
29
|
+
COMPOSITE_TRACER,
|
|
30
|
+
NoTracing,
|
|
31
|
+
ResumedTracing,
|
|
32
|
+
TracingModule,
|
|
33
|
+
frame_stack_read,
|
|
34
|
+
frame_stack_write,
|
|
35
|
+
)
|
|
36
|
+
from crosshair.util import (
|
|
37
|
+
CROSSHAIR_EXTRA_ASSERTS,
|
|
38
|
+
CrossHairInternal,
|
|
39
|
+
CrossHairValue,
|
|
40
|
+
debug,
|
|
41
|
+
)
|
|
42
|
+
from crosshair.z3util import z3Not, z3Or
|
|
43
|
+
|
|
44
|
+
BINARY_SUBSCR = dis.opmap.get("BINARY_SUBSCR", 256)
|
|
45
|
+
BINARY_SLICE = dis.opmap.get("BINARY_SLICE", 256)
|
|
46
|
+
BUILD_STRING = dis.opmap["BUILD_STRING"]
|
|
47
|
+
COMPARE_OP = dis.opmap["COMPARE_OP"]
|
|
48
|
+
CONTAINS_OP = dis.opmap.get("CONTAINS_OP", 256)
|
|
49
|
+
FORMAT_VALUE = dis.opmap.get("FORMAT_VALUE", 256)
|
|
50
|
+
CONVERT_VALUE = dis.opmap.get("CONVERT_VALUE", 256)
|
|
51
|
+
MAP_ADD = dis.opmap["MAP_ADD"]
|
|
52
|
+
SET_ADD = dis.opmap["SET_ADD"]
|
|
53
|
+
UNARY_NOT = dis.opmap["UNARY_NOT"]
|
|
54
|
+
TO_BOOL = dis.opmap.get("TO_BOOL", 256)
|
|
55
|
+
IS_OP = dis.opmap.get("IS_OP", 256)
|
|
56
|
+
BINARY_MODULO = dis.opmap.get("BINARY_MODULO", 256)
|
|
57
|
+
BINARY_OP = dis.opmap.get("BINARY_OP", 256)
|
|
58
|
+
LOAD_COMMON_CONSTANT = dis.opmap.get("LOAD_COMMON_CONSTANT", 256)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def frame_op_arg(frame):
|
|
62
|
+
return frame.f_code.co_code[frame.f_lasti + 1] # TODO: account for EXTENDED_ARG?
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_DEEPLY_CONCRETE_KEY_TYPES = (
|
|
66
|
+
int,
|
|
67
|
+
float,
|
|
68
|
+
str,
|
|
69
|
+
# Suble but important; when subscripting a Weak[Key|Value]Dictionary,
|
|
70
|
+
# we need to avoid creating a SimpleDict out of the backing dictionary.
|
|
71
|
+
# (because it can drop keys during iteration and fail)
|
|
72
|
+
weakref.ref,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MultiSubscriptableContainer:
|
|
77
|
+
"""Used for indexing a symbolic (non-slice) key into a concrete container"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, container: Union[list, tuple, dict]):
|
|
80
|
+
self.container = container
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, key: AtomicSymbolicValue) -> object:
|
|
83
|
+
with NoTracing():
|
|
84
|
+
space = context_statespace()
|
|
85
|
+
container = self.container
|
|
86
|
+
if isinstance(container, Mapping):
|
|
87
|
+
kv_pairs: Iterable[Tuple[Any, Any]] = container.items()
|
|
88
|
+
else:
|
|
89
|
+
kv_pairs = enumerate(container)
|
|
90
|
+
|
|
91
|
+
values_by_type = defaultdict(list)
|
|
92
|
+
values_by_id = {}
|
|
93
|
+
keys_by_value_id = defaultdict(list)
|
|
94
|
+
symbolic_for_pytype = space.extra(ModelingDirector).choose
|
|
95
|
+
for cur_key, cur_value in kv_pairs:
|
|
96
|
+
if (
|
|
97
|
+
isinstance(cur_value, AtomicSymbolicValue)
|
|
98
|
+
or type(cur_value) in python_types_using_atomic_symbolics()
|
|
99
|
+
):
|
|
100
|
+
pytype = (
|
|
101
|
+
cur_value._pytype()
|
|
102
|
+
if isinstance(cur_value, AtomicSymbolicValue)
|
|
103
|
+
else type(cur_value)
|
|
104
|
+
)
|
|
105
|
+
# Some types like real-based float and symbolic types don't cover all values:
|
|
106
|
+
if (
|
|
107
|
+
symbolic_for_pytype(pytype)._smt_promote_literal(cur_value)
|
|
108
|
+
is not None
|
|
109
|
+
):
|
|
110
|
+
values_by_type[pytype].append((cur_key, cur_value))
|
|
111
|
+
continue
|
|
112
|
+
# No symbolics cover this value, but we might still find repeated values:
|
|
113
|
+
values_by_id[id(cur_value)] = cur_value
|
|
114
|
+
keys_by_value_id[id(cur_value)].append(cur_key)
|
|
115
|
+
for value_type, cur_pairs in values_by_type.items():
|
|
116
|
+
hypothetical_result = symbolic_for_pytype(value_type)(
|
|
117
|
+
"item_" + space.uniq(), value_type
|
|
118
|
+
)
|
|
119
|
+
with ResumedTracing():
|
|
120
|
+
condition_pairs = []
|
|
121
|
+
for cur_key, cur_val in cur_pairs:
|
|
122
|
+
keys_equal = key == cur_key
|
|
123
|
+
values_equal = hypothetical_result == cur_val
|
|
124
|
+
with NoTracing():
|
|
125
|
+
if isinstance(keys_equal, SymbolicBool):
|
|
126
|
+
condition_pairs.append((keys_equal, values_equal))
|
|
127
|
+
elif keys_equal is False:
|
|
128
|
+
pass
|
|
129
|
+
else:
|
|
130
|
+
# (because the key must be symbolic, we don't ever expect raw True)
|
|
131
|
+
raise CrossHairInternal(
|
|
132
|
+
f"key comparison type: {type(keys_equal)} {keys_equal}"
|
|
133
|
+
)
|
|
134
|
+
if any(keys_equal for keys_equal, _ in condition_pairs):
|
|
135
|
+
space.add(any([all(pair) for pair in condition_pairs]))
|
|
136
|
+
return hypothetical_result
|
|
137
|
+
|
|
138
|
+
exprs_and_values: List[Tuple[ExprRef, object]] = []
|
|
139
|
+
for value_id, value in values_by_id.items():
|
|
140
|
+
keys_for_value = keys_by_value_id[value_id]
|
|
141
|
+
with ResumedTracing():
|
|
142
|
+
is_match = any([key == k for k in keys_for_value])
|
|
143
|
+
if isinstance(is_match, SymbolicBool):
|
|
144
|
+
exprs_and_values.append((is_match.var, value))
|
|
145
|
+
elif is_match:
|
|
146
|
+
return value
|
|
147
|
+
if exprs_and_values:
|
|
148
|
+
return space.smt_fanout(exprs_and_values, desc="multi_subscript")
|
|
149
|
+
|
|
150
|
+
if type(container) is dict:
|
|
151
|
+
raise KeyError # ( f"Key {key} not found in dict")
|
|
152
|
+
else:
|
|
153
|
+
raise IndexError # (f"Index {key} out of range for list/tuple of length {len(container)}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class LoadCommonConstantInterceptor(TracingModule):
|
|
157
|
+
"""
|
|
158
|
+
As of 3.14, the bytecode generation process generates optimizations
|
|
159
|
+
for builtins.any/all when invoked on a generator expression.
|
|
160
|
+
It essentially "inlines" the logic as bytecode.
|
|
161
|
+
We need to avoid this.
|
|
162
|
+
Before entering the optimized code path, it will check that the any/all
|
|
163
|
+
function is identity-equal to the original builtin, which is loaded using
|
|
164
|
+
the LOAD_COMMON_CONSTANT opcode.
|
|
165
|
+
|
|
166
|
+
This interceptor replaces that function with a proxy that functions
|
|
167
|
+
identically but is not identity-equal (so that we avoid the optimized
|
|
168
|
+
path),
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
opcodes_wanted = frozenset([LOAD_COMMON_CONSTANT])
|
|
172
|
+
|
|
173
|
+
def trace_op(self, frame, codeobj, codenum):
|
|
174
|
+
CONSTANT_BUILTIN_ALL = 3
|
|
175
|
+
CONSTANT_BUILTIN_ANY = 4
|
|
176
|
+
index = frame_op_arg(frame)
|
|
177
|
+
|
|
178
|
+
def post_op():
|
|
179
|
+
expected_fn = all if index == CONSTANT_BUILTIN_ALL else any
|
|
180
|
+
if CROSSHAIR_EXTRA_ASSERTS:
|
|
181
|
+
if frame_stack_read(frame, -1) is not expected_fn:
|
|
182
|
+
raise CrossHairInternal
|
|
183
|
+
frame_stack_write(frame, -1, lambda *a: expected_fn(*a))
|
|
184
|
+
|
|
185
|
+
if index == CONSTANT_BUILTIN_ALL or index == CONSTANT_BUILTIN_ANY:
|
|
186
|
+
COMPOSITE_TRACER.set_postop_callback(post_op, frame)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class SymbolicSubscriptInterceptor(TracingModule):
|
|
190
|
+
opcodes_wanted = frozenset([BINARY_SUBSCR, BINARY_OP])
|
|
191
|
+
|
|
192
|
+
def trace_op(self, frame, codeobj, codenum):
|
|
193
|
+
if codenum == BINARY_OP:
|
|
194
|
+
oparg = frame_op_arg(frame)
|
|
195
|
+
if oparg != 26: # subscript operator, NB_SUBSCR
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
key = frame_stack_read(frame, -1)
|
|
199
|
+
if isinstance(key, _DEEPLY_CONCRETE_KEY_TYPES):
|
|
200
|
+
return
|
|
201
|
+
# If we got this far, the index is likely symbolic (or perhaps a slice object)
|
|
202
|
+
container = frame_stack_read(frame, -2)
|
|
203
|
+
container_type = type(container)
|
|
204
|
+
if isinstance(key, AtomicSymbolicValue) and type(container) in (
|
|
205
|
+
tuple,
|
|
206
|
+
list,
|
|
207
|
+
dict,
|
|
208
|
+
):
|
|
209
|
+
wrapped_container = MultiSubscriptableContainer(container)
|
|
210
|
+
frame_stack_write(frame, -2, wrapped_container)
|
|
211
|
+
elif container_type is dict:
|
|
212
|
+
# SimpleDict won't hash the keys it's given!
|
|
213
|
+
wrapped_dict = SimpleDict(list(container.items()))
|
|
214
|
+
frame_stack_write(frame, -2, wrapped_dict)
|
|
215
|
+
elif isinstance(key, slice) and container_type is list:
|
|
216
|
+
step = key.step
|
|
217
|
+
if isinstance(step, CrossHairValue) or step not in (None, 1):
|
|
218
|
+
return
|
|
219
|
+
start, stop = key.start, key.stop
|
|
220
|
+
if isinstance(start, SymbolicInt) or isinstance(stop, SymbolicInt):
|
|
221
|
+
view_wrapper = SliceView(container, 0, len(container))
|
|
222
|
+
frame_stack_write(frame, -2, SymbolicList(view_wrapper))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class SymbolicSliceInterceptor(TracingModule):
|
|
226
|
+
opcodes_wanted = frozenset([BINARY_SLICE])
|
|
227
|
+
|
|
228
|
+
def trace_op(
|
|
229
|
+
self, frame, codeobj, codenum, _concrete_index_types=(int, float, str)
|
|
230
|
+
):
|
|
231
|
+
# Note that because this is called from inside a Python trace handler, tracing
|
|
232
|
+
# is automatically disabled, so there's no need for a `with NoTracing():` guard.
|
|
233
|
+
start = frame_stack_read(frame, -1)
|
|
234
|
+
stop = frame_stack_read(frame, -2)
|
|
235
|
+
if isinstance(start, _concrete_index_types) and isinstance(
|
|
236
|
+
stop, _concrete_index_types
|
|
237
|
+
):
|
|
238
|
+
return
|
|
239
|
+
# If we got this far, the index is likely symbolic (or perhaps a slice object)
|
|
240
|
+
container = frame_stack_read(frame, -3)
|
|
241
|
+
container_type = type(container)
|
|
242
|
+
if container_type is list:
|
|
243
|
+
if isinstance(start, SymbolicInt) or isinstance(stop, SymbolicInt):
|
|
244
|
+
view_wrapper = SliceView(container, 0, len(container))
|
|
245
|
+
frame_stack_write(frame, -3, SymbolicList(view_wrapper))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class DeoptimizedContainer:
|
|
249
|
+
def __init__(self, container):
|
|
250
|
+
self.container = container
|
|
251
|
+
|
|
252
|
+
def __contains__(self, other):
|
|
253
|
+
return self.container.__contains__(other)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class SideEffectStashingHashable:
|
|
257
|
+
def __init__(self, fn: Callable):
|
|
258
|
+
self.fn = fn
|
|
259
|
+
|
|
260
|
+
def __hash__(self):
|
|
261
|
+
self.result = self.fn()
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class DeoptimizedPercentFormattingStr:
|
|
266
|
+
def __init__(self, value):
|
|
267
|
+
self.value = value
|
|
268
|
+
|
|
269
|
+
def __mod__(self, other):
|
|
270
|
+
return self.value.__mod__(other)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class FormatStashingValue:
|
|
274
|
+
def __init__(self, value):
|
|
275
|
+
self.value = value
|
|
276
|
+
|
|
277
|
+
def __str__(self):
|
|
278
|
+
self.formatted = str(self.value)
|
|
279
|
+
return ""
|
|
280
|
+
|
|
281
|
+
def __format__(self, fmt: str):
|
|
282
|
+
self.formatted = format(self.value, fmt)
|
|
283
|
+
return ""
|
|
284
|
+
|
|
285
|
+
def __repr__(self) -> str:
|
|
286
|
+
self.formatted = repr(self.value)
|
|
287
|
+
return ""
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class BoolStashingValue:
|
|
291
|
+
def __init__(self, value, negate):
|
|
292
|
+
self.value = value
|
|
293
|
+
self.negate = negate
|
|
294
|
+
|
|
295
|
+
def __bool__(self):
|
|
296
|
+
stashed_bool = self.value.__bool__()
|
|
297
|
+
with NoTracing():
|
|
298
|
+
if self.negate:
|
|
299
|
+
if isinstance(stashed_bool, SymbolicBool):
|
|
300
|
+
self.stashed_bool = SymbolicBool(z3Not(stashed_bool.var))
|
|
301
|
+
else:
|
|
302
|
+
self.stashed_bool = not stashed_bool
|
|
303
|
+
else:
|
|
304
|
+
self.stashed_bool = stashed_bool
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
_CONTAINMENT_OP_TYPES = tuple(
|
|
309
|
+
i for (i, name) in enumerate(dis.cmp_op) if name in ("in", "not in")
|
|
310
|
+
)
|
|
311
|
+
assert len(_CONTAINMENT_OP_TYPES) in (0, 2)
|
|
312
|
+
|
|
313
|
+
_COMPARE_ISOP_TYPES = tuple(
|
|
314
|
+
i for (i, name) in enumerate(dis.cmp_op) if name in ("is", "is not")
|
|
315
|
+
)
|
|
316
|
+
assert len(_COMPARE_ISOP_TYPES) in (0, 2)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ComparisonInterceptForwarder(TracingModule):
|
|
320
|
+
|
|
321
|
+
opcodes_wanted = frozenset([COMPARE_OP])
|
|
322
|
+
|
|
323
|
+
def trace_op(self, frame, codeobj, codenum):
|
|
324
|
+
# Python 3.8 used a general purpose comparison opcode.
|
|
325
|
+
# Forward to dedicated opcode handlers as appropriate.
|
|
326
|
+
compare_type = frame_op_arg(frame)
|
|
327
|
+
if compare_type in _CONTAINMENT_OP_TYPES:
|
|
328
|
+
ContainmentInterceptor.trace_op(None, frame, codeobj, codenum)
|
|
329
|
+
elif compare_type in _COMPARE_ISOP_TYPES:
|
|
330
|
+
IdentityInterceptor.trace_op(None, frame, codeobj, codenum)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class ContainmentInterceptor(TracingModule):
|
|
334
|
+
|
|
335
|
+
opcodes_wanted = frozenset([CONTAINS_OP])
|
|
336
|
+
|
|
337
|
+
def trace_op(self, frame, codeobj, codenum):
|
|
338
|
+
item = frame_stack_read(frame, -2)
|
|
339
|
+
if not isinstance(item, CrossHairValue):
|
|
340
|
+
return
|
|
341
|
+
container = frame_stack_read(frame, -1)
|
|
342
|
+
containertype = type(container)
|
|
343
|
+
new_container = None
|
|
344
|
+
if containertype is str:
|
|
345
|
+
new_container = DeoptimizedContainer(container)
|
|
346
|
+
elif containertype is set:
|
|
347
|
+
new_container = ShellMutableSet(LinearSet(container))
|
|
348
|
+
elif containertype is dict:
|
|
349
|
+
new_container = SimpleDict(list(container.items()))
|
|
350
|
+
|
|
351
|
+
if new_container is not None:
|
|
352
|
+
frame_stack_write(frame, -1, new_container)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class BuildStringInterceptor(TracingModule):
|
|
356
|
+
"""
|
|
357
|
+
Adds symbolic handling for the BUILD_STRING opcode (used by f-strings).
|
|
358
|
+
|
|
359
|
+
BUILD_STRING concatenates strings from the stack is a fast, but unforgiving way:
|
|
360
|
+
it requires all the substrings to be real Python strings.
|
|
361
|
+
We work around this by replacing the substrings with empty strings, computing the
|
|
362
|
+
concatenation ourselves, and swaping our result in after the opcode completes.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
opcodes_wanted = frozenset([BUILD_STRING])
|
|
366
|
+
|
|
367
|
+
def trace_op(self, frame, codeobj, codenum):
|
|
368
|
+
count = frame_op_arg(frame)
|
|
369
|
+
real_result = ""
|
|
370
|
+
for offset in range(-(count), 0):
|
|
371
|
+
substr = frame_stack_read(frame, offset)
|
|
372
|
+
if not isinstance(substr, (str, AnySymbolicStr)):
|
|
373
|
+
raise CrossHairInternal
|
|
374
|
+
# Because we know these are all symbolic or concrete strings, it's ok to
|
|
375
|
+
# not have tracing on when we do the concatenation here:
|
|
376
|
+
real_result += substr
|
|
377
|
+
frame_stack_write(frame, offset, "")
|
|
378
|
+
|
|
379
|
+
def post_op():
|
|
380
|
+
frame_stack_write(frame, -1, real_result)
|
|
381
|
+
|
|
382
|
+
COMPOSITE_TRACER.set_postop_callback(post_op, frame)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class FormatValueInterceptor(TracingModule):
|
|
386
|
+
"""Avoid checks and realization during FORMAT_VALUE (used by f-strings)."""
|
|
387
|
+
|
|
388
|
+
# TODO: don't we need to handle FORMAT_SIMPLE and FORMAT_WITH_SPEC?
|
|
389
|
+
opcodes_wanted = frozenset([FORMAT_VALUE, CONVERT_VALUE])
|
|
390
|
+
|
|
391
|
+
def trace_op(self, frame, codeobj, codenum):
|
|
392
|
+
flags = frame_op_arg(frame)
|
|
393
|
+
value_idx = -2 if flags == 0x04 else -1
|
|
394
|
+
orig_obj = frame_stack_read(frame, value_idx)
|
|
395
|
+
|
|
396
|
+
# FORMAT_VALUE checks that results are concrete strings. So, we format via a
|
|
397
|
+
# a wrapper that returns an empty str, and then swap in the actual string later:
|
|
398
|
+
|
|
399
|
+
wrapper = FormatStashingValue(orig_obj)
|
|
400
|
+
if flags in (0x00, 0x01) and isinstance(orig_obj, AnySymbolicStr):
|
|
401
|
+
# Just use the symbolic string directly (don't bother formatting at all)
|
|
402
|
+
wrapper.formatted = orig_obj
|
|
403
|
+
frame_stack_write(frame, value_idx, "")
|
|
404
|
+
else:
|
|
405
|
+
frame_stack_write(frame, value_idx, wrapper)
|
|
406
|
+
|
|
407
|
+
def post_op():
|
|
408
|
+
frame_stack_write(frame, -1, wrapper.formatted)
|
|
409
|
+
|
|
410
|
+
COMPOSITE_TRACER.set_postop_callback(post_op, frame)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class MapAddInterceptor(TracingModule):
|
|
414
|
+
"""De-optimize MAP_ADD over symbolics (used in dict comprehensions)."""
|
|
415
|
+
|
|
416
|
+
opcodes_wanted = frozenset([MAP_ADD])
|
|
417
|
+
|
|
418
|
+
def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
|
|
419
|
+
dict_offset = -(frame_op_arg(frame) + 2)
|
|
420
|
+
dict_obj = frame_stack_read(frame, dict_offset)
|
|
421
|
+
if not isinstance(dict_obj, (dict, MutableMapping)):
|
|
422
|
+
raise CrossHairInternal
|
|
423
|
+
# Key and value were swapped in Python 3.8
|
|
424
|
+
key_offset, value_offset = (-2, -1) if version_info >= (3, 8) else (-1, -2)
|
|
425
|
+
key = frame_stack_read(frame, key_offset)
|
|
426
|
+
value = frame_stack_read(frame, value_offset)
|
|
427
|
+
if isinstance(dict_obj, dict):
|
|
428
|
+
if type(key) in ATOMIC_IMMUTABLE_TYPES:
|
|
429
|
+
# Dict and key is (deeply) concrete; continue as normal.
|
|
430
|
+
return
|
|
431
|
+
else:
|
|
432
|
+
dict_obj = SimpleDict(list(dict_obj.items()))
|
|
433
|
+
|
|
434
|
+
# Have the interpreter do a fake assinment.
|
|
435
|
+
# While the fake assignment happens, we'll perform the real assignment secretly
|
|
436
|
+
# when Python hashes the fake key.
|
|
437
|
+
def do_real_assignment():
|
|
438
|
+
dict_obj[key] = value
|
|
439
|
+
|
|
440
|
+
frame_stack_write(frame, dict_offset, {})
|
|
441
|
+
frame_stack_write(frame, value_offset, 1)
|
|
442
|
+
frame_stack_write(
|
|
443
|
+
frame, key_offset, SideEffectStashingHashable(do_real_assignment)
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Afterwards, overwrite the interpreter's resulting dict with ours:
|
|
447
|
+
def post_op():
|
|
448
|
+
old_dict_obj = frame_stack_read(frame, dict_offset + 2)
|
|
449
|
+
if CROSSHAIR_EXTRA_ASSERTS and not isinstance(
|
|
450
|
+
old_dict_obj, (dict, MutableMapping)
|
|
451
|
+
):
|
|
452
|
+
raise CrossHairInternal("interpreter stack corruption detected")
|
|
453
|
+
frame_stack_write(frame, dict_offset + 2, dict_obj)
|
|
454
|
+
|
|
455
|
+
COMPOSITE_TRACER.set_postop_callback(post_op, frame)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class ToBoolInterceptor(TracingModule):
|
|
459
|
+
"""Retain symbolic booleans across the TO_BOOL operator."""
|
|
460
|
+
|
|
461
|
+
opcodes_wanted = frozenset([TO_BOOL])
|
|
462
|
+
|
|
463
|
+
def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
|
|
464
|
+
input_bool = frame_stack_read(frame, -1)
|
|
465
|
+
if not isinstance(input_bool, CrossHairValue):
|
|
466
|
+
return
|
|
467
|
+
if isinstance(input_bool, SymbolicBool):
|
|
468
|
+
# TODO: right now, we define __bool__ methods to perform realization.
|
|
469
|
+
# At some point, if that isn't the case, and we can remove this specialized
|
|
470
|
+
# branch for `SybolicBool`.
|
|
471
|
+
frame_stack_write(frame, -1, True)
|
|
472
|
+
|
|
473
|
+
def post_op():
|
|
474
|
+
frame_stack_write(frame, -1, input_bool)
|
|
475
|
+
|
|
476
|
+
else:
|
|
477
|
+
stashing_value = BoolStashingValue(input_bool, negate=False)
|
|
478
|
+
frame_stack_write(frame, -1, stashing_value)
|
|
479
|
+
|
|
480
|
+
def post_op():
|
|
481
|
+
frame_stack_write(frame, -1, stashing_value.stashed_bool)
|
|
482
|
+
|
|
483
|
+
COMPOSITE_TRACER.set_postop_callback(post_op, frame)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class NotInterceptor(TracingModule):
|
|
487
|
+
"""Retain symbolic booleans across the `not` operator."""
|
|
488
|
+
|
|
489
|
+
opcodes_wanted = frozenset([UNARY_NOT])
|
|
490
|
+
|
|
491
|
+
def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
|
|
492
|
+
input_bool = frame_stack_read(frame, -1)
|
|
493
|
+
if not isinstance(input_bool, CrossHairValue):
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
if isinstance(input_bool, SymbolicBool):
|
|
497
|
+
# TODO: right now, we define __bool__ methods to perform realization.
|
|
498
|
+
# At some point, if that isn't the case, and we can remove this specialized
|
|
499
|
+
# branch for `SybolicBool`.
|
|
500
|
+
frame_stack_write(frame, -1, True)
|
|
501
|
+
|
|
502
|
+
def post_op():
|
|
503
|
+
frame_stack_write(frame, -1, SymbolicBool(z3Not(input_bool.var)))
|
|
504
|
+
|
|
505
|
+
else:
|
|
506
|
+
stashing_value = BoolStashingValue(input_bool, negate=True)
|
|
507
|
+
frame_stack_write(frame, -1, stashing_value)
|
|
508
|
+
|
|
509
|
+
def post_op():
|
|
510
|
+
frame_stack_write(frame, -1, stashing_value.stashed_bool)
|
|
511
|
+
|
|
512
|
+
COMPOSITE_TRACER.set_postop_callback(post_op, frame)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class SetAddInterceptor(TracingModule):
|
|
516
|
+
"""De-optimize SET_ADD over symbolics (used in set comprehensions)."""
|
|
517
|
+
|
|
518
|
+
opcodes_wanted = frozenset([SET_ADD])
|
|
519
|
+
|
|
520
|
+
def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
|
|
521
|
+
set_offset = -(frame_op_arg(frame) + 1)
|
|
522
|
+
set_obj = frame_stack_read(frame, set_offset)
|
|
523
|
+
if not isinstance(set_obj, Set):
|
|
524
|
+
raise CrossHairInternal(type(set_obj))
|
|
525
|
+
item = frame_stack_read(frame, -1)
|
|
526
|
+
if isinstance(set_obj, set):
|
|
527
|
+
if isinstance(item, CrossHairValue):
|
|
528
|
+
set_obj = ShellMutableSet(set_obj)
|
|
529
|
+
else:
|
|
530
|
+
# Set and value are concrete; continue as normal.
|
|
531
|
+
return
|
|
532
|
+
# Have the interpreter do a fake addition, namely `set().add(1)`
|
|
533
|
+
dummy_set: Set = set()
|
|
534
|
+
frame_stack_write(frame, set_offset, dummy_set)
|
|
535
|
+
frame_stack_write(frame, -1, 1)
|
|
536
|
+
|
|
537
|
+
# And do our own addition separately:
|
|
538
|
+
set_obj.add(item)
|
|
539
|
+
|
|
540
|
+
# Later, overwrite the interpreter's result with ours:
|
|
541
|
+
def post_op():
|
|
542
|
+
if CROSSHAIR_EXTRA_ASSERTS:
|
|
543
|
+
to_replace = frame_stack_read(frame, set_offset + 1)
|
|
544
|
+
if to_replace is not dummy_set:
|
|
545
|
+
raise CrossHairInternal(
|
|
546
|
+
f"Found an instance of {type(to_replace)} where dummy set should be."
|
|
547
|
+
)
|
|
548
|
+
frame_stack_write(frame, set_offset + 1, set_obj)
|
|
549
|
+
|
|
550
|
+
COMPOSITE_TRACER.set_postop_callback(post_op, frame)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class IdentityInterceptor(TracingModule):
|
|
554
|
+
"""Detect an "is" comparison to symbolics booleans"""
|
|
555
|
+
|
|
556
|
+
opcodes_wanted = frozenset([IS_OP])
|
|
557
|
+
# TODO: Adding support for an OptionalSymbolic would now be possible.
|
|
558
|
+
# TODO: it would be amazing to add symbolic enums and support comparison here
|
|
559
|
+
|
|
560
|
+
def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
|
|
561
|
+
arg1 = frame_stack_read(frame, -1)
|
|
562
|
+
arg2 = frame_stack_read(frame, -2)
|
|
563
|
+
if isinstance(arg1, SymbolicBool) and isinstance(arg2, (bool, SymbolicBool)):
|
|
564
|
+
frame_stack_write(frame, -1, arg1.__ch_realize__())
|
|
565
|
+
if isinstance(arg2, SymbolicBool) and isinstance(arg1, (bool, SymbolicBool)):
|
|
566
|
+
frame_stack_write(frame, -2, arg2.__ch_realize__())
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class ModuloInterceptor(TracingModule):
|
|
570
|
+
opcodes_wanted = frozenset([BINARY_MODULO, BINARY_OP])
|
|
571
|
+
assert BINARY_MODULO != BINARY_OP
|
|
572
|
+
|
|
573
|
+
def trace_op(self, frame, codeobj, codenum):
|
|
574
|
+
left = frame_stack_read(frame, -2)
|
|
575
|
+
from crosshair.util import debug
|
|
576
|
+
|
|
577
|
+
if isinstance(left, str):
|
|
578
|
+
if codenum == BINARY_OP:
|
|
579
|
+
oparg = frame_op_arg(frame)
|
|
580
|
+
if oparg != 6: # modulo operator, NB_REMAINDER
|
|
581
|
+
return
|
|
582
|
+
frame_stack_write(frame, -2, DeoptimizedPercentFormattingStr(left))
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def make_registrations():
|
|
586
|
+
register_opcode_patch(SymbolicSubscriptInterceptor())
|
|
587
|
+
if sys.version_info >= (3, 12):
|
|
588
|
+
register_opcode_patch(SymbolicSliceInterceptor())
|
|
589
|
+
if sys.version_info < (3, 9):
|
|
590
|
+
register_opcode_patch(ComparisonInterceptForwarder())
|
|
591
|
+
if sys.version_info >= (3, 14):
|
|
592
|
+
register_opcode_patch(LoadCommonConstantInterceptor())
|
|
593
|
+
register_opcode_patch(ContainmentInterceptor())
|
|
594
|
+
register_opcode_patch(BuildStringInterceptor())
|
|
595
|
+
register_opcode_patch(FormatValueInterceptor())
|
|
596
|
+
register_opcode_patch(MapAddInterceptor())
|
|
597
|
+
# register_opcode_patch(ToBoolInterceptor())
|
|
598
|
+
register_opcode_patch(NotInterceptor())
|
|
599
|
+
register_opcode_patch(SetAddInterceptor())
|
|
600
|
+
register_opcode_patch(IdentityInterceptor())
|
|
601
|
+
register_opcode_patch(ModuloInterceptor())
|