crosshair-tool 0.0.56__cp39-cp39-macosx_11_0_arm64.whl → 0.0.100__cp39-cp39-macosx_11_0_arm64.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-39-darwin.so +0 -0
- crosshair/__init__.py +1 -1
- crosshair/_mark_stacks.h +51 -24
- crosshair/_tracers.h +9 -5
- crosshair/_tracers_test.py +19 -9
- crosshair/auditwall.py +9 -8
- crosshair/auditwall_test.py +31 -19
- crosshair/codeconfig.py +3 -2
- crosshair/condition_parser.py +17 -133
- crosshair/condition_parser_test.py +54 -96
- crosshair/conftest.py +1 -1
- crosshair/copyext.py +91 -22
- crosshair/copyext_test.py +33 -0
- crosshair/core.py +259 -203
- crosshair/core_and_libs.py +20 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +693 -664
- crosshair/diff_behavior.py +76 -21
- crosshair/diff_behavior_test.py +132 -23
- crosshair/dynamic_typing.py +128 -18
- crosshair/dynamic_typing_test.py +91 -4
- crosshair/enforce.py +1 -6
- crosshair/enforce_test.py +15 -23
- crosshair/examples/check_examples_test.py +2 -1
- crosshair/fnutil.py +2 -3
- crosshair/fnutil_test.py +0 -7
- crosshair/fuzz_core_test.py +70 -83
- crosshair/libimpl/arraylib.py +10 -7
- 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 +5 -5
- crosshair/libimpl/builtinslib.py +1002 -682
- crosshair/libimpl/builtinslib_ch_test.py +108 -30
- crosshair/libimpl/builtinslib_test.py +431 -143
- crosshair/libimpl/codecslib.py +22 -2
- crosshair/libimpl/codecslib_test.py +41 -9
- crosshair/libimpl/collectionslib.py +44 -8
- crosshair/libimpl/collectionslib_test.py +108 -20
- crosshair/libimpl/copylib.py +1 -1
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +84 -67
- crosshair/libimpl/datetimelib_ch_test.py +12 -7
- crosshair/libimpl/datetimelib_test.py +5 -6
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/_encutil.py +21 -11
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +19 -7
- crosshair/libimpl/functoolslib_test.py +22 -6
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +32 -5
- crosshair/libimpl/heapqlib_test.py +15 -12
- crosshair/libimpl/iolib.py +7 -4
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib_test.py +1 -1
- crosshair/libimpl/mathlib.py +165 -2
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +59 -16
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +1 -0
- crosshair/libimpl/randomlib_test.py +6 -4
- crosshair/libimpl/relib.py +180 -59
- crosshair/libimpl/relib_ch_test.py +26 -2
- crosshair/libimpl/relib_test.py +77 -14
- crosshair/libimpl/timelib.py +35 -13
- crosshair/libimpl/timelib_test.py +13 -3
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib_test.py +3 -3
- 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 +21 -10
- crosshair/main.py +48 -28
- crosshair/main_test.py +59 -14
- crosshair/objectproxy.py +39 -14
- crosshair/objectproxy_test.py +27 -13
- crosshair/opcode_intercept.py +212 -24
- crosshair/opcode_intercept_test.py +172 -18
- crosshair/options.py +0 -1
- crosshair/patch_equivalence_test.py +5 -21
- crosshair/path_cover.py +7 -5
- crosshair/path_search.py +6 -4
- crosshair/path_search_test.py +1 -2
- crosshair/pathing_oracle.py +53 -10
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer_test.py +5 -21
- crosshair/register_contract.py +16 -6
- crosshair/register_contract_test.py +2 -14
- crosshair/simplestructs.py +154 -85
- crosshair/simplestructs_test.py +16 -2
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +319 -196
- crosshair/statespace_test.py +45 -0
- crosshair/stubs_parser.py +0 -2
- crosshair/test_util.py +87 -25
- crosshair/test_util_test.py +26 -0
- crosshair/tools/check_init_and_setup_coincide.py +0 -3
- crosshair/tools/generate_demo_table.py +2 -2
- crosshair/tracers.py +141 -49
- crosshair/type_repo.py +11 -4
- crosshair/unicode_categories.py +1 -0
- crosshair/util.py +158 -76
- crosshair/util_test.py +13 -20
- crosshair/watcher.py +4 -4
- crosshair/z3util.py +1 -1
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/METADATA +45 -36
- crosshair_tool-0.0.100.dist-info/RECORD +176 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/WHEEL +2 -1
- crosshair/examples/hypothesis/__init__.py +0 -2
- crosshair/examples/hypothesis/bugs_detected/simple_strategies.py +0 -74
- crosshair_tool-0.0.56.dist-info/RECORD +0 -152
- /crosshair/{examples/hypothesis/bugs_detected/__init__.py → py.typed} +0 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/entry_points.txt +0 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info/licenses}/LICENSE +0 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/top_level.txt +0 -0
crosshair/opcode_intercept.py
CHANGED
|
@@ -1,29 +1,47 @@
|
|
|
1
1
|
import dis
|
|
2
2
|
import sys
|
|
3
|
+
import weakref
|
|
4
|
+
from collections import defaultdict
|
|
3
5
|
from collections.abc import MutableMapping, Set
|
|
4
6
|
from sys import version_info
|
|
5
7
|
from types import CodeType, FrameType
|
|
6
|
-
from typing import Callable
|
|
8
|
+
from typing import Any, Callable, Iterable, List, Mapping, Tuple, Union
|
|
7
9
|
|
|
8
|
-
from
|
|
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
|
+
)
|
|
9
17
|
from crosshair.libimpl.builtinslib import (
|
|
10
18
|
AnySymbolicStr,
|
|
19
|
+
AtomicSymbolicValue,
|
|
20
|
+
ModelingDirector,
|
|
11
21
|
SymbolicBool,
|
|
12
22
|
SymbolicInt,
|
|
13
23
|
SymbolicList,
|
|
24
|
+
python_types_using_atomic_symbolics,
|
|
14
25
|
)
|
|
15
26
|
from crosshair.simplestructs import LinearSet, ShellMutableSet, SimpleDict, SliceView
|
|
27
|
+
from crosshair.statespace import context_statespace
|
|
16
28
|
from crosshair.tracers import (
|
|
17
29
|
COMPOSITE_TRACER,
|
|
18
30
|
NoTracing,
|
|
31
|
+
ResumedTracing,
|
|
19
32
|
TracingModule,
|
|
20
33
|
frame_stack_read,
|
|
21
34
|
frame_stack_write,
|
|
22
35
|
)
|
|
23
|
-
from crosshair.util import
|
|
24
|
-
|
|
36
|
+
from crosshair.util import (
|
|
37
|
+
CROSSHAIR_EXTRA_ASSERTS,
|
|
38
|
+
CrossHairInternal,
|
|
39
|
+
CrossHairValue,
|
|
40
|
+
debug,
|
|
41
|
+
)
|
|
42
|
+
from crosshair.z3util import z3Not, z3Or
|
|
25
43
|
|
|
26
|
-
BINARY_SUBSCR = dis.opmap
|
|
44
|
+
BINARY_SUBSCR = dis.opmap.get("BINARY_SUBSCR", 256)
|
|
27
45
|
BINARY_SLICE = dis.opmap.get("BINARY_SLICE", 256)
|
|
28
46
|
BUILD_STRING = dis.opmap["BUILD_STRING"]
|
|
29
47
|
COMPARE_OP = dis.opmap["COMPARE_OP"]
|
|
@@ -35,32 +53,166 @@ SET_ADD = dis.opmap["SET_ADD"]
|
|
|
35
53
|
UNARY_NOT = dis.opmap["UNARY_NOT"]
|
|
36
54
|
TO_BOOL = dis.opmap.get("TO_BOOL", 256)
|
|
37
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)
|
|
38
59
|
|
|
39
60
|
|
|
40
61
|
def frame_op_arg(frame):
|
|
41
62
|
return frame.f_code.co_code[frame.f_lasti + 1] # TODO: account for EXTENDED_ARG?
|
|
42
63
|
|
|
43
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
|
+
|
|
44
189
|
class SymbolicSubscriptInterceptor(TracingModule):
|
|
45
|
-
opcodes_wanted = frozenset([BINARY_SUBSCR])
|
|
190
|
+
opcodes_wanted = frozenset([BINARY_SUBSCR, BINARY_OP])
|
|
46
191
|
|
|
47
192
|
def trace_op(self, frame, codeobj, codenum):
|
|
48
|
-
|
|
49
|
-
|
|
193
|
+
if codenum == BINARY_OP:
|
|
194
|
+
oparg = frame_op_arg(frame)
|
|
195
|
+
if oparg != 26: # subscript operator, NB_SUBSCR
|
|
196
|
+
return
|
|
197
|
+
|
|
50
198
|
key = frame_stack_read(frame, -1)
|
|
51
|
-
if isinstance(key,
|
|
199
|
+
if isinstance(key, _DEEPLY_CONCRETE_KEY_TYPES):
|
|
52
200
|
return
|
|
53
201
|
# If we got this far, the index is likely symbolic (or perhaps a slice object)
|
|
54
202
|
container = frame_stack_read(frame, -2)
|
|
55
203
|
container_type = type(container)
|
|
56
|
-
if
|
|
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:
|
|
57
212
|
# SimpleDict won't hash the keys it's given!
|
|
58
213
|
wrapped_dict = SimpleDict(list(container.items()))
|
|
59
214
|
frame_stack_write(frame, -2, wrapped_dict)
|
|
60
|
-
elif container_type is list:
|
|
61
|
-
if not isinstance(key, slice):
|
|
62
|
-
# Nothing useful to do with concrete list and symbolic numeric index.
|
|
63
|
-
return
|
|
215
|
+
elif isinstance(key, slice) and container_type is list:
|
|
64
216
|
step = key.step
|
|
65
217
|
if isinstance(step, CrossHairValue) or step not in (None, 1):
|
|
66
218
|
return
|
|
@@ -110,6 +262,14 @@ class SideEffectStashingHashable:
|
|
|
110
262
|
return 0
|
|
111
263
|
|
|
112
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
|
+
|
|
113
273
|
class FormatStashingValue:
|
|
114
274
|
def __init__(self, value):
|
|
115
275
|
self.value = value
|
|
@@ -210,7 +370,7 @@ class BuildStringInterceptor(TracingModule):
|
|
|
210
370
|
for offset in range(-(count), 0):
|
|
211
371
|
substr = frame_stack_read(frame, offset)
|
|
212
372
|
if not isinstance(substr, (str, AnySymbolicStr)):
|
|
213
|
-
raise
|
|
373
|
+
raise CrossHairInternal
|
|
214
374
|
# Because we know these are all symbolic or concrete strings, it's ok to
|
|
215
375
|
# not have tracing on when we do the concatenation here:
|
|
216
376
|
real_result += substr
|
|
@@ -225,6 +385,7 @@ class BuildStringInterceptor(TracingModule):
|
|
|
225
385
|
class FormatValueInterceptor(TracingModule):
|
|
226
386
|
"""Avoid checks and realization during FORMAT_VALUE (used by f-strings)."""
|
|
227
387
|
|
|
388
|
+
# TODO: don't we need to handle FORMAT_SIMPLE and FORMAT_WITH_SPEC?
|
|
228
389
|
opcodes_wanted = frozenset([FORMAT_VALUE, CONVERT_VALUE])
|
|
229
390
|
|
|
230
391
|
def trace_op(self, frame, codeobj, codenum):
|
|
@@ -258,7 +419,7 @@ class MapAddInterceptor(TracingModule):
|
|
|
258
419
|
dict_offset = -(frame_op_arg(frame) + 2)
|
|
259
420
|
dict_obj = frame_stack_read(frame, dict_offset)
|
|
260
421
|
if not isinstance(dict_obj, (dict, MutableMapping)):
|
|
261
|
-
raise
|
|
422
|
+
raise CrossHairInternal
|
|
262
423
|
# Key and value were swapped in Python 3.8
|
|
263
424
|
key_offset, value_offset = (-2, -1) if version_info >= (3, 8) else (-1, -2)
|
|
264
425
|
key = frame_stack_read(frame, key_offset)
|
|
@@ -266,7 +427,6 @@ class MapAddInterceptor(TracingModule):
|
|
|
266
427
|
if isinstance(dict_obj, dict):
|
|
267
428
|
if type(key) in ATOMIC_IMMUTABLE_TYPES:
|
|
268
429
|
# Dict and key is (deeply) concrete; continue as normal.
|
|
269
|
-
COMPOSITE_TRACER.set_postop_callback(None, frame)
|
|
270
430
|
return
|
|
271
431
|
else:
|
|
272
432
|
dict_obj = SimpleDict(list(dict_obj.items()))
|
|
@@ -285,6 +445,11 @@ class MapAddInterceptor(TracingModule):
|
|
|
285
445
|
|
|
286
446
|
# Afterwards, overwrite the interpreter's resulting dict with ours:
|
|
287
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")
|
|
288
453
|
frame_stack_write(frame, dict_offset + 2, dict_obj)
|
|
289
454
|
|
|
290
455
|
COMPOSITE_TRACER.set_postop_callback(post_op, frame)
|
|
@@ -298,7 +463,6 @@ class ToBoolInterceptor(TracingModule):
|
|
|
298
463
|
def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
|
|
299
464
|
input_bool = frame_stack_read(frame, -1)
|
|
300
465
|
if not isinstance(input_bool, CrossHairValue):
|
|
301
|
-
COMPOSITE_TRACER.set_postop_callback(None, frame)
|
|
302
466
|
return
|
|
303
467
|
if isinstance(input_bool, SymbolicBool):
|
|
304
468
|
# TODO: right now, we define __bool__ methods to perform realization.
|
|
@@ -327,7 +491,6 @@ class NotInterceptor(TracingModule):
|
|
|
327
491
|
def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
|
|
328
492
|
input_bool = frame_stack_read(frame, -1)
|
|
329
493
|
if not isinstance(input_bool, CrossHairValue):
|
|
330
|
-
COMPOSITE_TRACER.set_postop_callback(None, frame)
|
|
331
494
|
return
|
|
332
495
|
|
|
333
496
|
if isinstance(input_bool, SymbolicBool):
|
|
@@ -358,17 +521,17 @@ class SetAddInterceptor(TracingModule):
|
|
|
358
521
|
set_offset = -(frame_op_arg(frame) + 1)
|
|
359
522
|
set_obj = frame_stack_read(frame, set_offset)
|
|
360
523
|
if not isinstance(set_obj, Set):
|
|
361
|
-
raise
|
|
524
|
+
raise CrossHairInternal(type(set_obj))
|
|
362
525
|
item = frame_stack_read(frame, -1)
|
|
363
526
|
if isinstance(set_obj, set):
|
|
364
527
|
if isinstance(item, CrossHairValue):
|
|
365
528
|
set_obj = ShellMutableSet(set_obj)
|
|
366
529
|
else:
|
|
367
530
|
# Set and value are concrete; continue as normal.
|
|
368
|
-
COMPOSITE_TRACER.set_postop_callback(None, frame)
|
|
369
531
|
return
|
|
370
532
|
# Have the interpreter do a fake addition, namely `set().add(1)`
|
|
371
|
-
|
|
533
|
+
dummy_set: Set = set()
|
|
534
|
+
frame_stack_write(frame, set_offset, dummy_set)
|
|
372
535
|
frame_stack_write(frame, -1, 1)
|
|
373
536
|
|
|
374
537
|
# And do our own addition separately:
|
|
@@ -376,6 +539,12 @@ class SetAddInterceptor(TracingModule):
|
|
|
376
539
|
|
|
377
540
|
# Later, overwrite the interpreter's result with ours:
|
|
378
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
|
+
)
|
|
379
548
|
frame_stack_write(frame, set_offset + 1, set_obj)
|
|
380
549
|
|
|
381
550
|
COMPOSITE_TRACER.set_postop_callback(post_op, frame)
|
|
@@ -391,18 +560,36 @@ class IdentityInterceptor(TracingModule):
|
|
|
391
560
|
def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
|
|
392
561
|
arg1 = frame_stack_read(frame, -1)
|
|
393
562
|
arg2 = frame_stack_read(frame, -2)
|
|
394
|
-
if isinstance(arg1, SymbolicBool):
|
|
563
|
+
if isinstance(arg1, SymbolicBool) and isinstance(arg2, (bool, SymbolicBool)):
|
|
395
564
|
frame_stack_write(frame, -1, arg1.__ch_realize__())
|
|
396
|
-
if isinstance(arg2, SymbolicBool):
|
|
565
|
+
if isinstance(arg2, SymbolicBool) and isinstance(arg1, (bool, SymbolicBool)):
|
|
397
566
|
frame_stack_write(frame, -2, arg2.__ch_realize__())
|
|
398
567
|
|
|
399
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
|
+
|
|
400
585
|
def make_registrations():
|
|
401
586
|
register_opcode_patch(SymbolicSubscriptInterceptor())
|
|
402
587
|
if sys.version_info >= (3, 12):
|
|
403
588
|
register_opcode_patch(SymbolicSliceInterceptor())
|
|
404
589
|
if sys.version_info < (3, 9):
|
|
405
590
|
register_opcode_patch(ComparisonInterceptForwarder())
|
|
591
|
+
if sys.version_info >= (3, 14):
|
|
592
|
+
register_opcode_patch(LoadCommonConstantInterceptor())
|
|
406
593
|
register_opcode_patch(ContainmentInterceptor())
|
|
407
594
|
register_opcode_patch(BuildStringInterceptor())
|
|
408
595
|
register_opcode_patch(FormatValueInterceptor())
|
|
@@ -411,3 +598,4 @@ def make_registrations():
|
|
|
411
598
|
register_opcode_patch(NotInterceptor())
|
|
412
599
|
register_opcode_patch(SetAddInterceptor())
|
|
413
600
|
register_opcode_patch(IdentityInterceptor())
|
|
601
|
+
register_opcode_patch(ModuloInterceptor())
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import math
|
|
1
2
|
import sys
|
|
3
|
+
from abc import ABCMeta
|
|
2
4
|
from typing import List, Set
|
|
3
5
|
|
|
4
6
|
import pytest
|
|
5
7
|
|
|
6
8
|
from crosshair.core_and_libs import NoTracing, proxy_for_type, standalone_statespace
|
|
7
|
-
from crosshair.
|
|
9
|
+
from crosshair.libimpl.builtinslib import (
|
|
10
|
+
ModelingDirector,
|
|
11
|
+
RealBasedSymbolicFloat,
|
|
12
|
+
SymbolicBool,
|
|
13
|
+
SymbolicInt,
|
|
14
|
+
SymbolicType,
|
|
15
|
+
)
|
|
16
|
+
from crosshair.statespace import POST_FAIL
|
|
8
17
|
from crosshair.test_util import check_states
|
|
18
|
+
from crosshair.tracers import ResumedTracing
|
|
9
19
|
from crosshair.z3util import z3And
|
|
10
20
|
|
|
11
21
|
|
|
@@ -22,6 +32,101 @@ def test_dict_index():
|
|
|
22
32
|
check_states(numstr, POST_FAIL)
|
|
23
33
|
|
|
24
34
|
|
|
35
|
+
def test_dict_index_without_realization(space):
|
|
36
|
+
class WithMeta(metaclass=ABCMeta):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
space.extra(ModelingDirector).global_representations[float] = RealBasedSymbolicFloat
|
|
40
|
+
a = {
|
|
41
|
+
-1: WithMeta,
|
|
42
|
+
# ^ tests regression: isinstance(WithMeta(), type) but type(WithMeta) != type
|
|
43
|
+
0: list,
|
|
44
|
+
1.0: 10.0,
|
|
45
|
+
2: 20,
|
|
46
|
+
3: 30,
|
|
47
|
+
4: 40,
|
|
48
|
+
("complex", "key"): 50,
|
|
49
|
+
6: math.inf,
|
|
50
|
+
7: math.inf,
|
|
51
|
+
}
|
|
52
|
+
int_key = proxy_for_type(int, "int_key")
|
|
53
|
+
int_key2 = proxy_for_type(int, "int_key2")
|
|
54
|
+
int_key3 = proxy_for_type(int, "int_key3")
|
|
55
|
+
float_key = RealBasedSymbolicFloat("float_key")
|
|
56
|
+
float_key2 = RealBasedSymbolicFloat("float_key2")
|
|
57
|
+
with ResumedTracing():
|
|
58
|
+
# Try some concrete values out first:
|
|
59
|
+
assert a[("complex", "key")] == 50
|
|
60
|
+
assert a[6] == float("inf")
|
|
61
|
+
try:
|
|
62
|
+
a[42]
|
|
63
|
+
assert False, "Expected KeyError for missing key 42"
|
|
64
|
+
except KeyError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
space.add(2 <= int_key)
|
|
68
|
+
space.add(int_key <= 4)
|
|
69
|
+
int_result = a[int_key]
|
|
70
|
+
assert space.is_possible(int_result == 20)
|
|
71
|
+
assert space.is_possible(int_result == 40)
|
|
72
|
+
assert not space.is_possible(int_result == 10)
|
|
73
|
+
space.add(float_key == 1.0)
|
|
74
|
+
float_result = a[float_key]
|
|
75
|
+
assert space.is_possible(float_result == 10.0)
|
|
76
|
+
assert not space.is_possible(float_result == 42.0)
|
|
77
|
+
space.add(float_key2 == 2.0)
|
|
78
|
+
float_result2 = a[float_key2]
|
|
79
|
+
assert space.is_possible(float_result2 == 20)
|
|
80
|
+
space.add(int_key2 == 0)
|
|
81
|
+
int_result2 = a[int_key2]
|
|
82
|
+
assert int_result2 == list
|
|
83
|
+
space.add(any([int_key3 == 6, int_key3 == 7]))
|
|
84
|
+
inf_result = a[int_key3]
|
|
85
|
+
assert inf_result is math.inf
|
|
86
|
+
assert isinstance(int_result, SymbolicInt)
|
|
87
|
+
assert isinstance(float_result, RealBasedSymbolicFloat)
|
|
88
|
+
assert isinstance(float_result2, SymbolicInt)
|
|
89
|
+
assert isinstance(int_result2, SymbolicType)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_dict_symbolic_index_miss(space):
|
|
93
|
+
a = {6: 60, 7: 70}
|
|
94
|
+
x = proxy_for_type(int, "x")
|
|
95
|
+
with ResumedTracing():
|
|
96
|
+
space.add(x <= 4)
|
|
97
|
+
with pytest.raises(KeyError):
|
|
98
|
+
result = a[x]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_concrete_list_with_symbolic_index_simple(space):
|
|
102
|
+
haystack = [False] * 13 + [True] + [False] * 11
|
|
103
|
+
|
|
104
|
+
idx = proxy_for_type(int, "idx")
|
|
105
|
+
with ResumedTracing():
|
|
106
|
+
space.add(0 <= idx)
|
|
107
|
+
space.add(idx < len(haystack))
|
|
108
|
+
ret = haystack[idx]
|
|
109
|
+
assert isinstance(ret, SymbolicBool)
|
|
110
|
+
with ResumedTracing():
|
|
111
|
+
assert space.is_possible(idx == 13)
|
|
112
|
+
assert space.is_possible(idx == 12)
|
|
113
|
+
space.add(ret)
|
|
114
|
+
assert not space.is_possible(idx == 12)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_concrete_list_with_symbolic_index_unhashable_values(space):
|
|
118
|
+
o1 = dict()
|
|
119
|
+
options = [o1, o1, o1]
|
|
120
|
+
idx = proxy_for_type(int, "idx")
|
|
121
|
+
with ResumedTracing():
|
|
122
|
+
space.add(0 <= idx)
|
|
123
|
+
space.add(idx < 3)
|
|
124
|
+
ret = options[idx]
|
|
125
|
+
assert ret is o1
|
|
126
|
+
assert space.is_possible(idx == 0)
|
|
127
|
+
assert space.is_possible(idx == 2)
|
|
128
|
+
|
|
129
|
+
|
|
25
130
|
def test_dict_key_containment():
|
|
26
131
|
abc = {"two": 2, "four": 4, "six": 6}
|
|
27
132
|
|
|
@@ -34,12 +139,12 @@ def test_dict_key_containment():
|
|
|
34
139
|
check_states(numstr, POST_FAIL)
|
|
35
140
|
|
|
36
141
|
|
|
37
|
-
def
|
|
142
|
+
def test_dict_comprehension_basic():
|
|
38
143
|
with standalone_statespace as space:
|
|
39
144
|
with NoTracing():
|
|
40
145
|
x = proxy_for_type(int, "x")
|
|
41
|
-
|
|
42
|
-
|
|
146
|
+
space.add(x >= 40)
|
|
147
|
+
space.add(x < 50)
|
|
43
148
|
d = {k: v for k, v in ((35, 3), (x, 4))}
|
|
44
149
|
with NoTracing():
|
|
45
150
|
assert type(d) is not dict
|
|
@@ -48,8 +153,8 @@ def test_dict_comprehension():
|
|
|
48
153
|
continue
|
|
49
154
|
with NoTracing():
|
|
50
155
|
assert type(k) is not int
|
|
51
|
-
assert space.is_possible(
|
|
52
|
-
assert space.is_possible(
|
|
156
|
+
assert space.is_possible(k == 43)
|
|
157
|
+
assert space.is_possible(k == 48)
|
|
53
158
|
|
|
54
159
|
|
|
55
160
|
def test_dict_comprehension_traces_during_custom_hash():
|
|
@@ -75,9 +180,8 @@ def test_dict_comprehension_traces_during_custom_hash():
|
|
|
75
180
|
# There is only one item:
|
|
76
181
|
assert len(d) == 1
|
|
77
182
|
# TODO: In theory, we shouldn't need to realize the string here (but we are):
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# assert space.is_possible(mystr.__len__().var == 1)
|
|
183
|
+
# assert space.is_possible(mystr.__len__() == 0)
|
|
184
|
+
# assert space.is_possible(mystr.__len__() == 1)
|
|
81
185
|
|
|
82
186
|
|
|
83
187
|
def test_dict_comprehension_e2e():
|
|
@@ -90,15 +194,17 @@ def test_dict_comprehension_e2e():
|
|
|
90
194
|
check_states(f, POST_FAIL)
|
|
91
195
|
|
|
92
196
|
|
|
197
|
+
@pytest.mark.skipif(
|
|
198
|
+
sys.version_info >= (3, 13), reason="Negation opcode changed; TODO: fix!"
|
|
199
|
+
)
|
|
93
200
|
def test_not_operator_on_bool():
|
|
94
201
|
with standalone_statespace as space:
|
|
95
202
|
with NoTracing():
|
|
96
203
|
boolval = proxy_for_type(bool, "boolval")
|
|
97
|
-
intlist = proxy_for_type(List[int], "intlist")
|
|
98
204
|
inverseval = not boolval
|
|
205
|
+
assert space.is_possible(inverseval)
|
|
99
206
|
with NoTracing():
|
|
100
207
|
assert type(inverseval) is not bool
|
|
101
|
-
assert space.is_possible(inverseval.var)
|
|
102
208
|
assert not space.is_possible(z3And(boolval.var, inverseval.var))
|
|
103
209
|
|
|
104
210
|
|
|
@@ -106,18 +212,18 @@ def test_not_operator_on_non_bool():
|
|
|
106
212
|
with standalone_statespace as space:
|
|
107
213
|
with NoTracing():
|
|
108
214
|
intlist = proxy_for_type(List[int], "intlist")
|
|
109
|
-
|
|
215
|
+
space.add(intlist.__len__() == 0)
|
|
110
216
|
notList = not intlist
|
|
111
217
|
with NoTracing():
|
|
112
218
|
assert notList
|
|
113
219
|
|
|
114
220
|
|
|
115
|
-
def
|
|
221
|
+
def test_set_comprehension_basic():
|
|
116
222
|
with standalone_statespace as space:
|
|
117
223
|
with NoTracing():
|
|
118
224
|
x = proxy_for_type(int, "x")
|
|
119
|
-
|
|
120
|
-
|
|
225
|
+
space.add(x >= 40)
|
|
226
|
+
space.add(x < 50)
|
|
121
227
|
result_set = {k for k in (35, x)}
|
|
122
228
|
with NoTracing():
|
|
123
229
|
assert type(result_set) is not set
|
|
@@ -126,8 +232,8 @@ def test_set_comprehension():
|
|
|
126
232
|
continue
|
|
127
233
|
with NoTracing():
|
|
128
234
|
assert type(k) is not int
|
|
129
|
-
assert space.is_possible(
|
|
130
|
-
assert space.is_possible(
|
|
235
|
+
assert space.is_possible(k == 43)
|
|
236
|
+
assert space.is_possible(k == 48)
|
|
131
237
|
|
|
132
238
|
|
|
133
239
|
def test_set_comprehension_e2e():
|
|
@@ -140,11 +246,59 @@ def test_set_comprehension_e2e():
|
|
|
140
246
|
check_states(f, POST_FAIL)
|
|
141
247
|
|
|
142
248
|
|
|
249
|
+
def test_trace_disabling_at_jump_targets(space):
|
|
250
|
+
# This replicates a corruption of the interpreter stack in 3.12
|
|
251
|
+
# under a specific bytecode layout.
|
|
252
|
+
#
|
|
253
|
+
# The origial issue was caused by neglecting to keep sys.monitor probes
|
|
254
|
+
# alive (for post-op callbacks) that could be jumped to from other
|
|
255
|
+
# locations.
|
|
256
|
+
_global_type_lookupx = {
|
|
257
|
+
1: 1,
|
|
258
|
+
bool: 2,
|
|
259
|
+
3: 3,
|
|
260
|
+
}
|
|
261
|
+
with ResumedTracing():
|
|
262
|
+
_ = {
|
|
263
|
+
k: v
|
|
264
|
+
for k, v in _global_type_lookupx.items() # <- a new line has to be here (yes, the generated bytecode differs!)
|
|
265
|
+
if k == bool # The iteration filter needs to alternate
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
143
269
|
# TODO: we could implement identity comparisons on 3.8 by intercepting COMPARE_OP
|
|
144
270
|
@pytest.mark.skipif(sys.version_info < (3, 9), reason="IS_OP is new in Python 3.9")
|
|
145
271
|
def test_identity_operator_on_booleans():
|
|
146
272
|
with standalone_statespace as space:
|
|
147
273
|
with NoTracing():
|
|
148
274
|
b1 = proxy_for_type(bool, "b1")
|
|
149
|
-
|
|
275
|
+
space.add(b1)
|
|
150
276
|
assert b1 is True
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@pytest.mark.skipif(sys.version_info < (3, 9), reason="IS_OP is new in Python 3.9")
|
|
280
|
+
def test_identity_operator_does_not_realize_on_differing_types():
|
|
281
|
+
with standalone_statespace as space:
|
|
282
|
+
with NoTracing():
|
|
283
|
+
b1 = proxy_for_type(bool, "b1")
|
|
284
|
+
choices_made_at_start = len(space.choices_made)
|
|
285
|
+
space.add(b1)
|
|
286
|
+
fourty_two = 42 # assignment just to avoid lint errors
|
|
287
|
+
b1 is fourty_two
|
|
288
|
+
assert len(space.choices_made) == choices_made_at_start
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class IExplodeOnRepr:
|
|
292
|
+
def __repr__(self):
|
|
293
|
+
raise ValueError("boom")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_postop_callback_skipped_on_exception_handler_jump(space):
|
|
297
|
+
with ResumedTracing():
|
|
298
|
+
elements = IExplodeOnRepr()
|
|
299
|
+
try:
|
|
300
|
+
ret = f"these are them: {elements!r}"
|
|
301
|
+
except ValueError: # pragma: no cover
|
|
302
|
+
ret = None
|
|
303
|
+
# need to do something(anything) with elements so that it's on the stack:
|
|
304
|
+
type(elements)
|