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,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)
|