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.
Files changed (176) hide show
  1. _crosshair_tracers.cpython-312-darwin.so +0 -0
  2. crosshair/__init__.py +42 -0
  3. crosshair/__main__.py +8 -0
  4. crosshair/_mark_stacks.h +790 -0
  5. crosshair/_preliminaries_test.py +18 -0
  6. crosshair/_tracers.h +94 -0
  7. crosshair/_tracers_pycompat.h +522 -0
  8. crosshair/_tracers_test.py +138 -0
  9. crosshair/abcstring.py +245 -0
  10. crosshair/auditwall.py +190 -0
  11. crosshair/auditwall_test.py +77 -0
  12. crosshair/codeconfig.py +113 -0
  13. crosshair/codeconfig_test.py +117 -0
  14. crosshair/condition_parser.py +1237 -0
  15. crosshair/condition_parser_test.py +497 -0
  16. crosshair/conftest.py +30 -0
  17. crosshair/copyext.py +155 -0
  18. crosshair/copyext_test.py +84 -0
  19. crosshair/core.py +1763 -0
  20. crosshair/core_and_libs.py +149 -0
  21. crosshair/core_regestered_types_test.py +82 -0
  22. crosshair/core_test.py +1316 -0
  23. crosshair/diff_behavior.py +314 -0
  24. crosshair/diff_behavior_test.py +261 -0
  25. crosshair/dynamic_typing.py +346 -0
  26. crosshair/dynamic_typing_test.py +210 -0
  27. crosshair/enforce.py +282 -0
  28. crosshair/enforce_test.py +182 -0
  29. crosshair/examples/PEP316/__init__.py +1 -0
  30. crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
  31. crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
  32. crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
  33. crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
  34. crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
  35. crosshair/examples/PEP316/correct_code/__init__.py +0 -0
  36. crosshair/examples/PEP316/correct_code/arith.py +60 -0
  37. crosshair/examples/PEP316/correct_code/chess.py +77 -0
  38. crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
  39. crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
  40. crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
  41. crosshair/examples/PEP316/correct_code/showcase.py +104 -0
  42. crosshair/examples/__init__.py +0 -0
  43. crosshair/examples/check_examples_test.py +146 -0
  44. crosshair/examples/deal/__init__.py +1 -0
  45. crosshair/examples/icontract/__init__.py +1 -0
  46. crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
  47. crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
  48. crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
  49. crosshair/examples/icontract/correct_code/__init__.py +0 -0
  50. crosshair/examples/icontract/correct_code/arith.py +51 -0
  51. crosshair/examples/icontract/correct_code/showcase.py +94 -0
  52. crosshair/fnutil.py +391 -0
  53. crosshair/fnutil_test.py +75 -0
  54. crosshair/fuzz_core_test.py +516 -0
  55. crosshair/libimpl/__init__.py +0 -0
  56. crosshair/libimpl/arraylib.py +161 -0
  57. crosshair/libimpl/binascii_ch_test.py +30 -0
  58. crosshair/libimpl/binascii_test.py +67 -0
  59. crosshair/libimpl/binasciilib.py +150 -0
  60. crosshair/libimpl/bisectlib_test.py +23 -0
  61. crosshair/libimpl/builtinslib.py +5228 -0
  62. crosshair/libimpl/builtinslib_ch_test.py +1191 -0
  63. crosshair/libimpl/builtinslib_test.py +3735 -0
  64. crosshair/libimpl/codecslib.py +86 -0
  65. crosshair/libimpl/codecslib_test.py +86 -0
  66. crosshair/libimpl/collectionslib.py +264 -0
  67. crosshair/libimpl/collectionslib_ch_test.py +252 -0
  68. crosshair/libimpl/collectionslib_test.py +332 -0
  69. crosshair/libimpl/copylib.py +23 -0
  70. crosshair/libimpl/copylib_test.py +18 -0
  71. crosshair/libimpl/datetimelib.py +2559 -0
  72. crosshair/libimpl/datetimelib_ch_test.py +354 -0
  73. crosshair/libimpl/datetimelib_test.py +112 -0
  74. crosshair/libimpl/decimallib.py +5257 -0
  75. crosshair/libimpl/decimallib_ch_test.py +78 -0
  76. crosshair/libimpl/decimallib_test.py +76 -0
  77. crosshair/libimpl/encodings/__init__.py +23 -0
  78. crosshair/libimpl/encodings/_encutil.py +187 -0
  79. crosshair/libimpl/encodings/ascii.py +44 -0
  80. crosshair/libimpl/encodings/latin_1.py +40 -0
  81. crosshair/libimpl/encodings/utf_8.py +93 -0
  82. crosshair/libimpl/encodings_ch_test.py +83 -0
  83. crosshair/libimpl/fractionlib.py +16 -0
  84. crosshair/libimpl/fractionlib_test.py +80 -0
  85. crosshair/libimpl/functoolslib.py +34 -0
  86. crosshair/libimpl/functoolslib_test.py +56 -0
  87. crosshair/libimpl/hashliblib.py +30 -0
  88. crosshair/libimpl/hashliblib_test.py +18 -0
  89. crosshair/libimpl/heapqlib.py +47 -0
  90. crosshair/libimpl/heapqlib_test.py +21 -0
  91. crosshair/libimpl/importliblib.py +18 -0
  92. crosshair/libimpl/importliblib_test.py +38 -0
  93. crosshair/libimpl/iolib.py +216 -0
  94. crosshair/libimpl/iolib_ch_test.py +128 -0
  95. crosshair/libimpl/iolib_test.py +19 -0
  96. crosshair/libimpl/ipaddresslib.py +8 -0
  97. crosshair/libimpl/itertoolslib.py +44 -0
  98. crosshair/libimpl/itertoolslib_test.py +44 -0
  99. crosshair/libimpl/jsonlib.py +984 -0
  100. crosshair/libimpl/jsonlib_ch_test.py +42 -0
  101. crosshair/libimpl/jsonlib_test.py +51 -0
  102. crosshair/libimpl/mathlib.py +179 -0
  103. crosshair/libimpl/mathlib_ch_test.py +44 -0
  104. crosshair/libimpl/mathlib_test.py +67 -0
  105. crosshair/libimpl/oslib.py +7 -0
  106. crosshair/libimpl/pathliblib_test.py +10 -0
  107. crosshair/libimpl/randomlib.py +178 -0
  108. crosshair/libimpl/randomlib_test.py +120 -0
  109. crosshair/libimpl/relib.py +846 -0
  110. crosshair/libimpl/relib_ch_test.py +169 -0
  111. crosshair/libimpl/relib_test.py +493 -0
  112. crosshair/libimpl/timelib.py +72 -0
  113. crosshair/libimpl/timelib_test.py +82 -0
  114. crosshair/libimpl/typeslib.py +15 -0
  115. crosshair/libimpl/typeslib_test.py +36 -0
  116. crosshair/libimpl/unicodedatalib.py +75 -0
  117. crosshair/libimpl/unicodedatalib_test.py +42 -0
  118. crosshair/libimpl/urlliblib.py +23 -0
  119. crosshair/libimpl/urlliblib_test.py +19 -0
  120. crosshair/libimpl/weakreflib.py +13 -0
  121. crosshair/libimpl/weakreflib_test.py +69 -0
  122. crosshair/libimpl/zliblib.py +15 -0
  123. crosshair/libimpl/zliblib_test.py +13 -0
  124. crosshair/lsp_server.py +261 -0
  125. crosshair/lsp_server_test.py +30 -0
  126. crosshair/main.py +973 -0
  127. crosshair/main_test.py +543 -0
  128. crosshair/objectproxy.py +376 -0
  129. crosshair/objectproxy_test.py +41 -0
  130. crosshair/opcode_intercept.py +601 -0
  131. crosshair/opcode_intercept_test.py +304 -0
  132. crosshair/options.py +218 -0
  133. crosshair/options_test.py +10 -0
  134. crosshair/patch_equivalence_test.py +75 -0
  135. crosshair/path_cover.py +209 -0
  136. crosshair/path_cover_test.py +138 -0
  137. crosshair/path_search.py +161 -0
  138. crosshair/path_search_test.py +52 -0
  139. crosshair/pathing_oracle.py +271 -0
  140. crosshair/pathing_oracle_test.py +21 -0
  141. crosshair/pure_importer.py +27 -0
  142. crosshair/pure_importer_test.py +16 -0
  143. crosshair/py.typed +0 -0
  144. crosshair/register_contract.py +273 -0
  145. crosshair/register_contract_test.py +190 -0
  146. crosshair/simplestructs.py +1165 -0
  147. crosshair/simplestructs_test.py +283 -0
  148. crosshair/smtlib.py +24 -0
  149. crosshair/smtlib_test.py +14 -0
  150. crosshair/statespace.py +1199 -0
  151. crosshair/statespace_test.py +108 -0
  152. crosshair/stubs_parser.py +352 -0
  153. crosshair/stubs_parser_test.py +43 -0
  154. crosshair/test_util.py +329 -0
  155. crosshair/test_util_test.py +26 -0
  156. crosshair/tools/__init__.py +0 -0
  157. crosshair/tools/check_help_in_doc.py +264 -0
  158. crosshair/tools/check_init_and_setup_coincide.py +119 -0
  159. crosshair/tools/generate_demo_table.py +127 -0
  160. crosshair/tracers.py +544 -0
  161. crosshair/tracers_test.py +154 -0
  162. crosshair/type_repo.py +151 -0
  163. crosshair/unicode_categories.py +589 -0
  164. crosshair/unicode_categories_test.py +27 -0
  165. crosshair/util.py +741 -0
  166. crosshair/util_test.py +173 -0
  167. crosshair/watcher.py +307 -0
  168. crosshair/watcher_test.py +107 -0
  169. crosshair/z3util.py +76 -0
  170. crosshair/z3util_test.py +11 -0
  171. crosshair_tool-0.0.99.dist-info/METADATA +144 -0
  172. crosshair_tool-0.0.99.dist-info/RECORD +176 -0
  173. crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
  174. crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
  175. crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
  176. crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
@@ -0,0 +1,138 @@
1
+ import dis
2
+ import gc
3
+ import sys
4
+ from typing import List
5
+
6
+ import pytest
7
+
8
+ from _crosshair_tracers import ( # type: ignore
9
+ CTracer,
10
+ code_stack_depths,
11
+ frame_stack_read,
12
+ )
13
+ from crosshair.util import mem_usage_kb
14
+
15
+
16
+ class ExampleModule:
17
+ opcodes_wanted = frozenset([42, 255])
18
+
19
+
20
+ def test_CTracer_module_refcounts_dont_leak():
21
+ mod = ExampleModule()
22
+ base_count = sys.getrefcount(mod)
23
+ tracer = CTracer()
24
+ tracer.push_module(mod)
25
+ assert sys.getrefcount(mod) == base_count + 1
26
+ tracer.push_module(mod)
27
+ tracer.start()
28
+ tracer.stop()
29
+ assert sys.getrefcount(mod) == base_count + 2
30
+ tracer.pop_module(mod)
31
+ assert sys.getrefcount(mod) == base_count + 1
32
+ del tracer
33
+ gc.collect()
34
+ assert sys.getrefcount(mod) == base_count
35
+
36
+
37
+ def _get_depths(fn):
38
+ # dis.dis(fn)
39
+ depths = code_stack_depths(fn.__code__)
40
+ for instr in dis.Bytecode(fn):
41
+ wordpos = instr.offset // 2
42
+ depth = depths[wordpos]
43
+ if depth != -9:
44
+ assert depth >= 0
45
+ assert depth + dis.stack_effect(instr.opcode, instr.arg) >= 0
46
+ return depths
47
+
48
+
49
+ class RawNull:
50
+ def __repr__(self):
51
+ return "_RAW_NULL"
52
+
53
+
54
+ _RAW_NULL = RawNull()
55
+
56
+
57
+ def _log_execution_stacks(fn, *a, **kw):
58
+ depths = _get_depths(fn)
59
+ stacks = []
60
+
61
+ def _tracer(frame, event, arg):
62
+ if event == "opcode":
63
+ lasti = frame.f_lasti
64
+ opcode = frame.f_code.co_code[lasti]
65
+ oparg = frame.f_code.co_code[lasti + 1] # TODO: account for EXTENDED_ARG
66
+ opname = dis.opname[opcode]
67
+ entry: List = [f"{opname}({oparg})"]
68
+ for i in range(-depths[lasti // 2], 0, 1):
69
+ try:
70
+ entry.append(frame_stack_read(frame, i))
71
+ except ValueError:
72
+ entry.append(_RAW_NULL)
73
+ stacks.append(tuple(entry))
74
+ frame.f_trace = _tracer
75
+ frame.f_trace_opcodes = True
76
+ return _tracer
77
+
78
+ old_tracer = sys.gettrace()
79
+ # Caller needs opcode tracing since Python 3.12; see https://github.com/python/cpython/issues/103615
80
+ sys._getframe().f_trace_opcodes = True
81
+
82
+ sys.settrace(_tracer)
83
+ try:
84
+ result = (fn(*a, **kw), None)
85
+ except Exception as exc:
86
+ result = (None, exc)
87
+ finally:
88
+ sys.settrace(old_tracer)
89
+ return stacks
90
+
91
+
92
+ @pytest.mark.skipif(
93
+ sys.version_info < (3, 12) or sys.version_info >= (3, 14),
94
+ reason="stack depths only in 3.12 & 3.13",
95
+ )
96
+ def test_one_function_stack_depth():
97
+ _E = (TypeError, KeyboardInterrupt)
98
+
99
+ def a(x):
100
+ return {k for k in (35, x)}
101
+
102
+ # just enure no crashes:
103
+ _log_execution_stacks(a, 4)
104
+
105
+
106
+ @pytest.mark.skipif(
107
+ sys.version_info < (3, 12) or sys.version_info >= (3, 14),
108
+ reason="stack depths only in 3.12 & 3.13",
109
+ )
110
+ def test_stack_get():
111
+ def to_be_traced(x):
112
+ r = 8 - x
113
+ return 9 - r
114
+
115
+ stacks = _log_execution_stacks(to_be_traced, 3)
116
+ assert ("BINARY_OP(10)", 8, 3) in stacks
117
+ assert ("BINARY_OP(10)", 9, 5) in stacks
118
+
119
+
120
+ @pytest.mark.skipif(
121
+ sys.platform.startswith("win"), reason="getrusage not available on windows"
122
+ )
123
+ def test_CTracer_does_not_leak_memory():
124
+ import resource # (available only on unix; delay import)
125
+
126
+ for i in range(1_000):
127
+ tracer = CTracer()
128
+ tracer.start()
129
+ mods = [ExampleModule() for _ in range(6)]
130
+ for mod in mods:
131
+ tracer.push_module(mod)
132
+ for mod in reversed(mods):
133
+ tracer.pop_module(mod)
134
+ tracer.stop()
135
+ if i == 100:
136
+ usage = mem_usage_kb()
137
+ usage_increase = mem_usage_kb() - usage
138
+ assert usage_increase < 200
crosshair/abcstring.py ADDED
@@ -0,0 +1,245 @@
1
+ import collections.abc
2
+ import sys
3
+ from collections import UserString
4
+ from numbers import Integral
5
+
6
+ from crosshair.tracers import NoTracing
7
+
8
+ # Similar to UserString, but allows you to lazily supply the contents
9
+ # when accessed.
10
+
11
+ # Sadly, this illusion doesn't fully work: various Python operations
12
+ # require a actual strings or subclasses.
13
+ # (see related issue: https://bugs.python.org/issue16397)
14
+
15
+ # TODO: Our symbolic strings likely already override most of these methods.
16
+ # Consider removing this class.
17
+
18
+ _MISSING = object()
19
+
20
+
21
+ def _real_string(thing: object):
22
+ with NoTracing():
23
+ return thing.data if isinstance(thing, (UserString, AbcString)) else thing
24
+
25
+
26
+ def _real_int(thing: object):
27
+ return thing.__int__() if isinstance(thing, Integral) else thing
28
+
29
+
30
+ class AbcString(collections.abc.Sequence, collections.abc.Hashable):
31
+ """
32
+ Implement just ``__str__``.
33
+
34
+ Useful for making lazy strings.
35
+ """
36
+
37
+ data = property(lambda s: s.__str__())
38
+
39
+ def __repr__(self):
40
+ return repr(self.data)
41
+
42
+ def __hash__(self):
43
+ return hash(self.data)
44
+
45
+ def __eq__(self, string):
46
+ return self.data == _real_string(string)
47
+
48
+ def __lt__(self, string):
49
+ return self.data < _real_string(string)
50
+
51
+ def __le__(self, string):
52
+ return self.data <= _real_string(string)
53
+
54
+ def __gt__(self, string):
55
+ return self.data > _real_string(string)
56
+
57
+ def __ge__(self, string):
58
+ return self.data >= _real_string(string)
59
+
60
+ def __contains__(self, char):
61
+ return _real_string(char) in self.data
62
+
63
+ def __len__(self):
64
+ return len(self.data)
65
+
66
+ def __getitem__(self, index):
67
+ return self.data[index]
68
+
69
+ def __add__(self, other):
70
+ other = _real_string(other)
71
+ if isinstance(other, str):
72
+ return self.data + other
73
+ return self.data + str(other)
74
+
75
+ def __radd__(self, other):
76
+ other = _real_string(other)
77
+ if isinstance(other, str):
78
+ return other + self.data
79
+ return str(other) + self.data
80
+
81
+ def __mul__(self, n):
82
+ return self.data * n
83
+
84
+ def __rmul__(self, n):
85
+ return self.data * n
86
+
87
+ def __mod__(self, args):
88
+ return self.data % args
89
+
90
+ def __rmod__(self, template):
91
+ return str(template) % self.data
92
+
93
+ # the following methods are defined in alphabetical order:
94
+ def capitalize(self):
95
+ return self.data.capitalize()
96
+
97
+ def casefold(self):
98
+ return self.data.casefold()
99
+
100
+ def center(self, width, *args):
101
+ return self.data.center(width, *args)
102
+
103
+ def count(self, sub, start=0, end=sys.maxsize):
104
+ return self.data.count(_real_string(sub), start, end)
105
+
106
+ def encode(self, encoding=_MISSING, errors=_MISSING):
107
+ if encoding is not _MISSING:
108
+ if errors is not _MISSING:
109
+ return self.data.encode(encoding, errors)
110
+ return self.data.encode(encoding)
111
+ return self.data.encode()
112
+
113
+ def endswith(self, suffix, start=0, end=sys.maxsize):
114
+ return self.data.endswith(suffix, start, end)
115
+
116
+ def expandtabs(self, tabsize=8):
117
+ return self.data.expandtabs(_real_int(tabsize))
118
+
119
+ def find(self, sub, start=0, end=sys.maxsize):
120
+ return self.data.find(_real_string(sub), start, end)
121
+
122
+ def format(self, *args, **kwds):
123
+ return self.data.format(*args, **kwds)
124
+
125
+ def format_map(self, mapping):
126
+ return self.data.format_map(mapping)
127
+
128
+ def index(self, sub, start=0, end=sys.maxsize):
129
+ return self.data.index(_real_string(sub), start, end)
130
+
131
+ def isalpha(self):
132
+ return self.data.isalpha()
133
+
134
+ def isalnum(self):
135
+ return self.data.isalnum()
136
+
137
+ def isascii(self):
138
+ return self.data.isascii()
139
+
140
+ def isdecimal(self):
141
+ return self.data.isdecimal()
142
+
143
+ def isdigit(self):
144
+ return self.data.isdigit()
145
+
146
+ def isidentifier(self):
147
+ return self.data.isidentifier()
148
+
149
+ def islower(self):
150
+ return self.data.islower()
151
+
152
+ def isnumeric(self):
153
+ return self.data.isnumeric()
154
+
155
+ def isprintable(self):
156
+ return self.data.isprintable()
157
+
158
+ def isspace(self):
159
+ return self.data.isspace()
160
+
161
+ def istitle(self):
162
+ return self.data.istitle()
163
+
164
+ def isupper(self):
165
+ return self.data.isupper()
166
+
167
+ def join(self, seq):
168
+ return self.data.join(seq)
169
+
170
+ def ljust(self, width, *args):
171
+ return self.data.ljust(width, *args)
172
+
173
+ def lower(self):
174
+ return self.data.lower()
175
+
176
+ def lstrip(self, chars=None):
177
+ return self.data.lstrip(_real_string(chars))
178
+
179
+ maketrans = str.maketrans
180
+
181
+ def partition(self, sep):
182
+ return self.data.partition(_real_string(sep))
183
+
184
+ def replace(self, old, new, maxsplit=-1):
185
+ return self.data.replace(_real_string(old), _real_string(new), maxsplit)
186
+
187
+ def rfind(self, sub, start=0, end=sys.maxsize):
188
+ return self.data.rfind(_real_string(sub), start, end)
189
+
190
+ def rindex(self, sub, start=0, end=sys.maxsize):
191
+ return self.data.rindex(_real_string(sub), start, end)
192
+
193
+ def rjust(self, width, *args):
194
+ return self.data.rjust(width, *args)
195
+
196
+ def rpartition(self, sep):
197
+ return self.data.rpartition(sep)
198
+
199
+ def rsplit(self, sep=None, maxsplit=-1):
200
+ return self.data.rsplit(sep, maxsplit)
201
+
202
+ def rstrip(self, chars=None):
203
+ return self.data.rstrip(_real_string(chars))
204
+
205
+ def split(self, sep=None, maxsplit=-1):
206
+ return self.data.split(sep, maxsplit)
207
+
208
+ def splitlines(self, keepends=False):
209
+ return self.data.splitlines(keepends)
210
+
211
+ def startswith(self, prefix, start=0, end=sys.maxsize):
212
+ return self.data.startswith(prefix, start, end)
213
+
214
+ def strip(self, chars=None):
215
+ return self.data.strip(_real_string(chars))
216
+
217
+ def swapcase(self):
218
+ return self.data.swapcase()
219
+
220
+ def title(self):
221
+ return self.data.title()
222
+
223
+ def translate(self, *args):
224
+ return self.data.translate(*args)
225
+
226
+ def upper(self):
227
+ return self.data.upper()
228
+
229
+ def zfill(self, width):
230
+ return self.data.zfill(width)
231
+
232
+ if sys.version_info >= (3, 9):
233
+
234
+ def removeprefix(self, prefix: str) -> "AbcString":
235
+ if self.startswith(prefix):
236
+ return self[len(prefix) :]
237
+ return self
238
+
239
+ def removesuffix(self, suffix: str) -> "AbcString":
240
+ if self.endswith(suffix):
241
+ suffixlen = len(suffix)
242
+ if suffixlen == 0:
243
+ return self
244
+ return self[:-suffixlen]
245
+ return self
crosshair/auditwall.py ADDED
@@ -0,0 +1,190 @@
1
+ import importlib
2
+ import inspect
3
+ import os
4
+ import sys
5
+ import traceback
6
+ from contextlib import contextmanager
7
+ from types import ModuleType
8
+ from typing import Callable, Dict, Generator, Iterable, Optional, Set, Tuple
9
+
10
+
11
+ class SideEffectDetected(Exception):
12
+ pass
13
+
14
+
15
+ _BLOCKED_OPEN_FLAGS = (
16
+ os.O_WRONLY | os.O_RDWR | os.O_APPEND | os.O_CREAT | os.O_EXCL | os.O_TRUNC
17
+ )
18
+
19
+
20
+ def accept(event: str, args: Tuple) -> None:
21
+ pass
22
+
23
+
24
+ def reject(event: str, args: Tuple) -> None:
25
+ raise SideEffectDetected(
26
+ f'A "{event}{args}" operation was detected. '
27
+ f"CrossHair should not be run on code with side effects"
28
+ )
29
+
30
+
31
+ def inside_module(modules: Iterable[ModuleType]) -> bool:
32
+ """Checks whether the current call stack is inside one of the given modules."""
33
+ for frame, _lineno in traceback.walk_stack(None):
34
+ frame_module = inspect.getmodule(frame)
35
+ if frame_module and frame_module in modules:
36
+ return True
37
+ return False
38
+
39
+
40
+ def check_open(event: str, args: Tuple) -> None:
41
+ (filename_or_descriptor, mode, flags) = args
42
+ if filename_or_descriptor in ("/dev/null", "nul"):
43
+ # (no-op writes on unix/windows)
44
+ return
45
+ if flags & _BLOCKED_OPEN_FLAGS:
46
+ raise SideEffectDetected(
47
+ f'We\'ve blocked a file writing operation on "{filename_or_descriptor}". '
48
+ f"CrossHair should not be run on code with side effects"
49
+ )
50
+
51
+
52
+ def check_msvcrt_open(event: str, args: Tuple) -> None:
53
+ print(args)
54
+ (handle, flags) = args
55
+ if flags & _BLOCKED_OPEN_FLAGS:
56
+ raise SideEffectDetected(
57
+ f'We\'ve blocked a file writing operation on "{handle}". '
58
+ f"CrossHair should not be run on code with side effects"
59
+ )
60
+
61
+
62
+ _MODULES_THAT_CAN_POPEN: Optional[Set[ModuleType]] = None
63
+
64
+
65
+ def modules_with_allowed_subprocess():
66
+ global _MODULES_THAT_CAN_POPEN
67
+ if _MODULES_THAT_CAN_POPEN is None:
68
+ allowed_module_names = ("_aix_support", "ctypes", "platform", "uuid")
69
+ _MODULES_THAT_CAN_POPEN = set()
70
+ for module_name in allowed_module_names:
71
+ try:
72
+ _MODULES_THAT_CAN_POPEN.add(importlib.import_module(module_name))
73
+ except ImportError:
74
+ pass
75
+ return _MODULES_THAT_CAN_POPEN
76
+
77
+
78
+ def check_subprocess(event: str, args: Tuple) -> None:
79
+ if not inside_module(modules_with_allowed_subprocess()):
80
+ reject(event, args)
81
+
82
+
83
+ _SPECIAL_HANDLERS = {
84
+ "open": check_open,
85
+ "subprocess.Popen": check_subprocess,
86
+ "os.posix_spawn": check_subprocess,
87
+ "msvcrt.open_osfhandle": check_msvcrt_open,
88
+ }
89
+
90
+
91
+ def make_handler(event: str) -> Callable[[str, Tuple], None]:
92
+ special_handler = _SPECIAL_HANDLERS.get(event, None)
93
+ if special_handler:
94
+ return special_handler
95
+ # Block certain events
96
+ if event in (
97
+ "winreg.CreateKey",
98
+ "winreg.DeleteKey",
99
+ "winreg.DeleteValue",
100
+ "winreg.SaveKey",
101
+ "winreg.SetValue",
102
+ "winreg.DisableReflectionKey",
103
+ "winreg.EnableReflectionKey",
104
+ ):
105
+ return reject
106
+ # Allow certain events.
107
+ if event in (
108
+ # These seem not terribly dangerous to allow:
109
+ "os.putenv",
110
+ "os.unsetenv",
111
+ "msvcrt.heapmin",
112
+ "msvcrt.kbhit",
113
+ # These involve I/O, but are hopefully non-destructive:
114
+ "glob.glob",
115
+ "msvcrt.get_osfhandle",
116
+ "msvcrt.setmode",
117
+ "os.listdir", # (important for Python's importer)
118
+ "os.scandir", # (important for Python's importer)
119
+ "os.chdir",
120
+ "os.fwalk",
121
+ "os.getxattr",
122
+ "os.listxattr",
123
+ "os.walk",
124
+ "pathlib.Path.glob",
125
+ "socket.gethostbyname", # (FastAPI TestClient uses this)
126
+ "socket.__new__", # (FastAPI TestClient uses this)
127
+ "socket.bind", # pygls's asyncio needs this on windows
128
+ "socket.connect", # pygls's asyncio needs this on windows
129
+ ):
130
+ return accept
131
+ # Block groups of events.
132
+ event_prefix = event.split(".", 1)[0]
133
+ if event_prefix in (
134
+ "os",
135
+ "fcntl",
136
+ "ftplib",
137
+ "glob",
138
+ "imaplib",
139
+ "msvcrt",
140
+ "nntplib",
141
+ "pathlib",
142
+ "poplib",
143
+ "shutil",
144
+ "smtplib",
145
+ "socket",
146
+ "sqlite3",
147
+ "subprocess",
148
+ "telnetlib",
149
+ "urllib",
150
+ "webbrowser",
151
+ ):
152
+ return reject
153
+ # Allow other events.
154
+ return accept
155
+
156
+
157
+ _HANDLERS: Dict[str, Callable[[str, Tuple], None]] = {}
158
+ _ENABLED = True
159
+
160
+
161
+ def audithook(event: str, args: Tuple) -> None:
162
+ if not _ENABLED:
163
+ return
164
+ handler = _HANDLERS.get(event)
165
+ if handler is None:
166
+ handler = make_handler(event)
167
+ _HANDLERS[event] = handler
168
+ handler(event, args)
169
+
170
+
171
+ @contextmanager
172
+ def opened_auditwall() -> Generator:
173
+ global _ENABLED
174
+ assert _ENABLED
175
+ _ENABLED = False
176
+ try:
177
+ yield
178
+ finally:
179
+ _ENABLED = True
180
+
181
+
182
+ def engage_auditwall() -> None:
183
+ sys.dont_write_bytecode = True # disable .pyc file writing
184
+ sys.addaudithook(audithook)
185
+
186
+
187
+ def disable_auditwall() -> None:
188
+ global _ENABLED
189
+ assert _ENABLED
190
+ _ENABLED = False
@@ -0,0 +1,77 @@
1
+ import os
2
+ import platform
3
+ import sys
4
+ import urllib.request
5
+ from subprocess import call
6
+
7
+ import pytest
8
+
9
+ from crosshair.auditwall import SideEffectDetected, engage_auditwall
10
+
11
+ # audit hooks cannot be uninstalled, and we don't want to wall off the
12
+ # testing process. Spawn subprcoesses instead.
13
+
14
+ pyexec = sys.executable
15
+
16
+
17
+ def test_fs_read_allowed():
18
+ assert call([pyexec, __file__, "read_open", "withwall"]) != 10
19
+
20
+
21
+ def test_scandir_allowed():
22
+ assert call([pyexec, __file__, "scandir", "withwall"]) == 0
23
+
24
+
25
+ def test_import_allowed():
26
+ assert call([pyexec, __file__, "import", "withwall"]) == 0
27
+
28
+
29
+ def test_fs_write_disallowed():
30
+ assert call([pyexec, __file__, "write_open", "withwall"]) == 10
31
+
32
+
33
+ def test_http_disallowed():
34
+ assert call([pyexec, __file__, "http", "withwall"]) == 10
35
+
36
+
37
+ def test_unlink_disallowed():
38
+ assert call([pyexec, __file__, "unlink", "withwall"]) == 10
39
+
40
+
41
+ def test_popen_disallowed():
42
+ assert call([pyexec, __file__, "popen", "withwall"]) == 10
43
+
44
+
45
+ def test_chdir_allowed():
46
+ assert call([pyexec, __file__, "chdir", "withwall"]) == 0
47
+
48
+
49
+ @pytest.mark.skipif(sys.version_info < (3, 9), reason="Python 3.9+ required")
50
+ def test_popen_via_platform_allowed():
51
+ assert call([pyexec, __file__, "popen_via_platform", "withwall"]) == 0
52
+
53
+
54
+ _ACTIONS = {
55
+ "read_open": lambda: open("/dev/null", "rb"),
56
+ "scandir": lambda: os.scandir("."),
57
+ "import": lambda: __import__("shutil"),
58
+ "write_open": lambda: open("/.auditwall.testwrite.txt", "w"),
59
+ "http": lambda: urllib.request.urlopen("http://localhost/foo"),
60
+ "unlink": lambda: os.unlink("./delme.txt"),
61
+ "popen": lambda: call(["echo", "hello"]),
62
+ "popen_via_platform": lambda: platform._syscmd_ver( # type: ignore
63
+ supported_platforms=(sys.platform,)
64
+ ),
65
+ "chdir": lambda: os.chdir("."),
66
+ }
67
+
68
+ if __name__ == "__main__":
69
+ action, wall = sys.argv[1:]
70
+ if wall == "withwall":
71
+ engage_auditwall()
72
+
73
+ try:
74
+ _ACTIONS[action]()
75
+ except SideEffectDetected as e:
76
+ print(e)
77
+ sys.exit(10)