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.
Files changed (123) hide show
  1. _crosshair_tracers.cpython-39-darwin.so +0 -0
  2. crosshair/__init__.py +1 -1
  3. crosshair/_mark_stacks.h +51 -24
  4. crosshair/_tracers.h +9 -5
  5. crosshair/_tracers_test.py +19 -9
  6. crosshair/auditwall.py +9 -8
  7. crosshair/auditwall_test.py +31 -19
  8. crosshair/codeconfig.py +3 -2
  9. crosshair/condition_parser.py +17 -133
  10. crosshair/condition_parser_test.py +54 -96
  11. crosshair/conftest.py +1 -1
  12. crosshair/copyext.py +91 -22
  13. crosshair/copyext_test.py +33 -0
  14. crosshair/core.py +259 -203
  15. crosshair/core_and_libs.py +20 -0
  16. crosshair/core_regestered_types_test.py +82 -0
  17. crosshair/core_test.py +693 -664
  18. crosshair/diff_behavior.py +76 -21
  19. crosshair/diff_behavior_test.py +132 -23
  20. crosshair/dynamic_typing.py +128 -18
  21. crosshair/dynamic_typing_test.py +91 -4
  22. crosshair/enforce.py +1 -6
  23. crosshair/enforce_test.py +15 -23
  24. crosshair/examples/check_examples_test.py +2 -1
  25. crosshair/fnutil.py +2 -3
  26. crosshair/fnutil_test.py +0 -7
  27. crosshair/fuzz_core_test.py +70 -83
  28. crosshair/libimpl/arraylib.py +10 -7
  29. crosshair/libimpl/binascii_ch_test.py +30 -0
  30. crosshair/libimpl/binascii_test.py +67 -0
  31. crosshair/libimpl/binasciilib.py +150 -0
  32. crosshair/libimpl/bisectlib_test.py +5 -5
  33. crosshair/libimpl/builtinslib.py +1002 -682
  34. crosshair/libimpl/builtinslib_ch_test.py +108 -30
  35. crosshair/libimpl/builtinslib_test.py +431 -143
  36. crosshair/libimpl/codecslib.py +22 -2
  37. crosshair/libimpl/codecslib_test.py +41 -9
  38. crosshair/libimpl/collectionslib.py +44 -8
  39. crosshair/libimpl/collectionslib_test.py +108 -20
  40. crosshair/libimpl/copylib.py +1 -1
  41. crosshair/libimpl/copylib_test.py +18 -0
  42. crosshair/libimpl/datetimelib.py +84 -67
  43. crosshair/libimpl/datetimelib_ch_test.py +12 -7
  44. crosshair/libimpl/datetimelib_test.py +5 -6
  45. crosshair/libimpl/decimallib.py +5257 -0
  46. crosshair/libimpl/decimallib_ch_test.py +78 -0
  47. crosshair/libimpl/decimallib_test.py +76 -0
  48. crosshair/libimpl/encodings/_encutil.py +21 -11
  49. crosshair/libimpl/fractionlib.py +16 -0
  50. crosshair/libimpl/fractionlib_test.py +80 -0
  51. crosshair/libimpl/functoolslib.py +19 -7
  52. crosshair/libimpl/functoolslib_test.py +22 -6
  53. crosshair/libimpl/hashliblib.py +30 -0
  54. crosshair/libimpl/hashliblib_test.py +18 -0
  55. crosshair/libimpl/heapqlib.py +32 -5
  56. crosshair/libimpl/heapqlib_test.py +15 -12
  57. crosshair/libimpl/iolib.py +7 -4
  58. crosshair/libimpl/ipaddresslib.py +8 -0
  59. crosshair/libimpl/itertoolslib_test.py +1 -1
  60. crosshair/libimpl/mathlib.py +165 -2
  61. crosshair/libimpl/mathlib_ch_test.py +44 -0
  62. crosshair/libimpl/mathlib_test.py +59 -16
  63. crosshair/libimpl/oslib.py +7 -0
  64. crosshair/libimpl/pathliblib_test.py +10 -0
  65. crosshair/libimpl/randomlib.py +1 -0
  66. crosshair/libimpl/randomlib_test.py +6 -4
  67. crosshair/libimpl/relib.py +180 -59
  68. crosshair/libimpl/relib_ch_test.py +26 -2
  69. crosshair/libimpl/relib_test.py +77 -14
  70. crosshair/libimpl/timelib.py +35 -13
  71. crosshair/libimpl/timelib_test.py +13 -3
  72. crosshair/libimpl/typeslib.py +15 -0
  73. crosshair/libimpl/typeslib_test.py +36 -0
  74. crosshair/libimpl/unicodedatalib_test.py +3 -3
  75. crosshair/libimpl/weakreflib.py +13 -0
  76. crosshair/libimpl/weakreflib_test.py +69 -0
  77. crosshair/libimpl/zliblib.py +15 -0
  78. crosshair/libimpl/zliblib_test.py +13 -0
  79. crosshair/lsp_server.py +21 -10
  80. crosshair/main.py +48 -28
  81. crosshair/main_test.py +59 -14
  82. crosshair/objectproxy.py +39 -14
  83. crosshair/objectproxy_test.py +27 -13
  84. crosshair/opcode_intercept.py +212 -24
  85. crosshair/opcode_intercept_test.py +172 -18
  86. crosshair/options.py +0 -1
  87. crosshair/patch_equivalence_test.py +5 -21
  88. crosshair/path_cover.py +7 -5
  89. crosshair/path_search.py +6 -4
  90. crosshair/path_search_test.py +1 -2
  91. crosshair/pathing_oracle.py +53 -10
  92. crosshair/pathing_oracle_test.py +21 -0
  93. crosshair/pure_importer_test.py +5 -21
  94. crosshair/register_contract.py +16 -6
  95. crosshair/register_contract_test.py +2 -14
  96. crosshair/simplestructs.py +154 -85
  97. crosshair/simplestructs_test.py +16 -2
  98. crosshair/smtlib.py +24 -0
  99. crosshair/smtlib_test.py +14 -0
  100. crosshair/statespace.py +319 -196
  101. crosshair/statespace_test.py +45 -0
  102. crosshair/stubs_parser.py +0 -2
  103. crosshair/test_util.py +87 -25
  104. crosshair/test_util_test.py +26 -0
  105. crosshair/tools/check_init_and_setup_coincide.py +0 -3
  106. crosshair/tools/generate_demo_table.py +2 -2
  107. crosshair/tracers.py +141 -49
  108. crosshair/type_repo.py +11 -4
  109. crosshair/unicode_categories.py +1 -0
  110. crosshair/util.py +158 -76
  111. crosshair/util_test.py +13 -20
  112. crosshair/watcher.py +4 -4
  113. crosshair/z3util.py +1 -1
  114. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/METADATA +45 -36
  115. crosshair_tool-0.0.100.dist-info/RECORD +176 -0
  116. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/WHEEL +2 -1
  117. crosshair/examples/hypothesis/__init__.py +0 -2
  118. crosshair/examples/hypothesis/bugs_detected/simple_strategies.py +0 -74
  119. crosshair_tool-0.0.56.dist-info/RECORD +0 -152
  120. /crosshair/{examples/hypothesis/bugs_detected/__init__.py → py.typed} +0 -0
  121. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/entry_points.txt +0 -0
  122. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info/licenses}/LICENSE +0 -0
  123. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/top_level.txt +0 -0
Binary file
crosshair/__init__.py CHANGED
@@ -15,7 +15,7 @@ from crosshair.statespace import StateSpace
15
15
  from crosshair.tracers import NoTracing, ResumedTracing
16
16
  from crosshair.util import IgnoreAttempt, debug
17
17
 
18
- __version__ = "0.0.56" # Do not forget to update in setup.py!
18
+ __version__ = "0.0.100" # Do not forget to update in setup.py!
19
19
  __author__ = "Phillip Schanely"
20
20
  __license__ = "MIT"
21
21
  __status__ = "Alpha"
crosshair/_mark_stacks.h CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
  // This file includes a modified version of CPython's mark_stacks
6
- // implementation fomr:
6
+ // implementation from:
7
7
  // https://github.com/python/cpython/blob/v3.12.0/Objects/frameobject.c
8
8
 
9
9
  // The shared source code is licensed under the PSF license and is
@@ -36,6 +36,7 @@ _ch_pop_to_level(int64_t stack, int level) {
36
36
  // Python 3.13
37
37
  // ===========
38
38
 
39
+ // from Include/internal/pycore_opcode_metadata.h
39
40
  const uint8_t _ch_PyOpcode_Caches[256] = {
40
41
  [JUMP_BACKWARD] = 1,
41
42
  [TO_BOOL] = 3,
@@ -48,6 +49,7 @@ const uint8_t _ch_PyOpcode_Caches[256] = {
48
49
  [LOAD_SUPER_ATTR] = 1,
49
50
  [LOAD_ATTR] = 9,
50
51
  [COMPARE_OP] = 1,
52
+ [CONTAINS_OP] = 1,
51
53
  [POP_JUMP_IF_TRUE] = 1,
52
54
  [POP_JUMP_IF_FALSE] = 1,
53
55
  [POP_JUMP_IF_NONE] = 1,
@@ -57,6 +59,7 @@ const uint8_t _ch_PyOpcode_Caches[256] = {
57
59
  [BINARY_OP] = 1,
58
60
  };
59
61
 
62
+ // from Include/internal/pycore_opcode_metadata.h
60
63
  const uint8_t _ch_PyOpcode_Deopt[256] = {
61
64
  [BEFORE_ASYNC_WITH] = BEFORE_ASYNC_WITH,
62
65
  [BEFORE_WITH] = BEFORE_WITH,
@@ -87,6 +90,7 @@ const uint8_t _ch_PyOpcode_Deopt[256] = {
87
90
  [CALL] = CALL,
88
91
  [CALL_ALLOC_AND_ENTER_INIT] = CALL,
89
92
  [CALL_BOUND_METHOD_EXACT_ARGS] = CALL,
93
+ [CALL_BOUND_METHOD_GENERAL] = CALL,
90
94
  [CALL_BUILTIN_CLASS] = CALL,
91
95
  [CALL_BUILTIN_FAST] = CALL,
92
96
  [CALL_BUILTIN_FAST_WITH_KEYWORDS] = CALL,
@@ -102,8 +106,9 @@ const uint8_t _ch_PyOpcode_Deopt[256] = {
102
106
  [CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = CALL,
103
107
  [CALL_METHOD_DESCRIPTOR_NOARGS] = CALL,
104
108
  [CALL_METHOD_DESCRIPTOR_O] = CALL,
109
+ [CALL_NON_PY_GENERAL] = CALL,
105
110
  [CALL_PY_EXACT_ARGS] = CALL,
106
- [CALL_PY_WITH_DEFAULTS] = CALL,
111
+ [CALL_PY_GENERAL] = CALL,
107
112
  [CALL_STR_1] = CALL,
108
113
  [CALL_TUPLE_1] = CALL,
109
114
  [CALL_TYPE_1] = CALL,
@@ -115,6 +120,8 @@ const uint8_t _ch_PyOpcode_Deopt[256] = {
115
120
  [COMPARE_OP_INT] = COMPARE_OP,
116
121
  [COMPARE_OP_STR] = COMPARE_OP,
117
122
  [CONTAINS_OP] = CONTAINS_OP,
123
+ [CONTAINS_OP_DICT] = CONTAINS_OP,
124
+ [CONTAINS_OP_SET] = CONTAINS_OP,
118
125
  [CONVERT_VALUE] = CONVERT_VALUE,
119
126
  [COPY] = COPY,
120
127
  [COPY_FREE_VARS] = COPY_FREE_VARS,
@@ -268,6 +275,7 @@ const uint8_t _ch_PyOpcode_Deopt[256] = {
268
275
  [YIELD_VALUE] = YIELD_VALUE,
269
276
  };
270
277
 
278
+ // from Python/instrumentation.c
271
279
  static const uint8_t _ch_DE_INSTRUMENT[256] = {
272
280
  [INSTRUMENTED_RESUME] = RESUME,
273
281
  [INSTRUMENTED_RETURN_VALUE] = RETURN_VALUE,
@@ -530,26 +538,13 @@ static const uint8_t _ch_DE_INSTRUMENT[256] = {
530
538
  #endif
531
539
  #endif
532
540
 
533
- /* Get the underlying opcode, stripping instrumentation */
534
- int _ch_Py_GetBaseOpcode(PyCodeObject *code, int i)
535
- {
536
- int opcode = _PyCode_CODE(code)[i].op.code;
537
- if (opcode == INSTRUMENTED_LINE) {
538
- opcode = code->_co_monitoring->lines[i].original_opcode;
539
- }
540
- if (opcode == INSTRUMENTED_INSTRUCTION) {
541
- opcode = code->_co_monitoring->per_instruction_opcodes[i];
542
- }
543
- int deinstrumented = _ch_DE_INSTRUMENT[opcode];
544
- if (deinstrumented) {
545
- return deinstrumented;
546
- }
547
- return _ch_PyOpcode_Deopt[opcode];
548
- }
549
-
550
541
  static int64_t *
551
542
  _ch_mark_stacks(PyCodeObject *code_obj, int len)
552
543
  {
544
+ // This differs from the corresponding function in CPython in a few ways:
545
+ // 1) The returned stack values represent stack depths, not stack content types.
546
+ // 2) The low bit of each depth is a flag that indicates whether this is an
547
+ // instruction that we trace, or it could follow an instruction that we trace.
553
548
  PyObject *co_code = PyCode_GetCode(code_obj);
554
549
  // printf("co_code %d\n", co_code);
555
550
  if (co_code == NULL) {
@@ -557,34 +552,47 @@ _ch_mark_stacks(PyCodeObject *code_obj, int len)
557
552
  }
558
553
  _Py_CODEUNIT *code = (_Py_CODEUNIT *)PyBytes_AS_STRING(co_code);
559
554
  int64_t *stacks = PyMem_New(int64_t, len+1);
560
- int i, j, opcode;
561
-
562
555
  if (stacks == NULL) {
563
556
  PyErr_NoMemory();
564
557
  Py_DECREF(co_code);
565
558
  return NULL;
566
559
  }
560
+ uint8_t *enabled_tracing = PyMem_New(uint8_t, len+1);
561
+ if (enabled_tracing == NULL) {
562
+ PyErr_NoMemory();
563
+ PyMem_Free(stacks);
564
+ Py_DECREF(co_code);
565
+ return NULL;
566
+ }
567
+ int i, j, opcode;
568
+
567
569
  for (int i = 1; i <= len; i++) {
568
570
  stacks[i] = UNINITIALIZED;
571
+ enabled_tracing[i] = 0;
569
572
  }
570
573
  stacks[0] = EMPTY_STACK;
574
+ #if PY_VERSION_HEX < 0x030D0000
575
+ // Python 3.12
571
576
  if (code_obj->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR))
572
577
  {
573
578
  // Generators get sent None while starting:
574
579
  stacks[0]++;
575
580
  }
581
+ #endif
576
582
  int todo = 1;
577
583
  while (todo) {
578
584
  todo = 0;
579
585
  /* Scan instructions */
580
586
  for (i = 0; i < len;) {
581
587
  int64_t next_stack = stacks[i];
582
- opcode = _ch_Py_GetBaseOpcode(code_obj, i);
588
+ opcode = code[i].op.code;
589
+ uint8_t trace_enabled_here = _ch_TRACABLE_INSTRUCTIONS[opcode];
590
+ enabled_tracing[i] |= trace_enabled_here;
583
591
  int oparg = 0;
584
592
  while (opcode == EXTENDED_ARG) {
585
593
  oparg = (oparg << 8) | code[i].op.arg;
586
594
  i++;
587
- opcode = _ch_Py_GetBaseOpcode(code_obj, i);
595
+ opcode = code[i].op.code;
588
596
  stacks[i] = next_stack;
589
597
  }
590
598
  int next_i = i + _ch_PyOpcode_Caches[opcode] + 1;
@@ -607,6 +615,7 @@ _ch_mark_stacks(PyCodeObject *code_obj, int len)
607
615
  target_stack = next_stack;
608
616
  assert(stacks[j] == UNINITIALIZED || stacks[j] == target_stack);
609
617
  stacks[j] = target_stack;
618
+ enabled_tracing[j] |= trace_enabled_here;
610
619
  stacks[next_i] = next_stack;
611
620
  break;
612
621
  }
@@ -615,6 +624,7 @@ _ch_mark_stacks(PyCodeObject *code_obj, int len)
615
624
  assert(j < len);
616
625
  assert(stacks[j] == UNINITIALIZED || stacks[j] == next_stack);
617
626
  stacks[j] = next_stack;
627
+ enabled_tracing[j] |= trace_enabled_here;
618
628
  stacks[next_i] = next_stack;
619
629
  break;
620
630
  case JUMP_FORWARD:
@@ -622,6 +632,7 @@ _ch_mark_stacks(PyCodeObject *code_obj, int len)
622
632
  assert(j < len);
623
633
  assert(stacks[j] == UNINITIALIZED || stacks[j] == next_stack);
624
634
  stacks[j] = next_stack;
635
+ enabled_tracing[j] |= trace_enabled_here;
625
636
  break;
626
637
  case JUMP_BACKWARD:
627
638
  case JUMP_BACKWARD_NO_INTERRUPT:
@@ -633,6 +644,7 @@ _ch_mark_stacks(PyCodeObject *code_obj, int len)
633
644
  }
634
645
  assert(stacks[j] == UNINITIALIZED || stacks[j] == next_stack);
635
646
  stacks[j] = next_stack;
647
+ enabled_tracing[j] |= trace_enabled_here;
636
648
  break;
637
649
  case GET_ITER:
638
650
  case GET_AITER:
@@ -646,10 +658,18 @@ _ch_mark_stacks(PyCodeObject *code_obj, int len)
646
658
  assert(j < len);
647
659
  assert(stacks[j] == UNINITIALIZED || stacks[j] == target_stack);
648
660
  stacks[j] = target_stack;
661
+ enabled_tracing[j] |= trace_enabled_here;
649
662
  break;
650
663
  }
651
664
  case END_ASYNC_FOR:
665
+ #if PY_VERSION_HEX < 0x030D0000
666
+ // Python 3.12
667
+ next_stack--;
668
+ #else
669
+ // Python 3.13+
652
670
  next_stack--;
671
+ next_stack--;
672
+ #endif
653
673
  stacks[next_i] = next_stack;
654
674
  break;
655
675
  case PUSH_EXC_INFO:
@@ -676,10 +696,10 @@ _ch_mark_stacks(PyCodeObject *code_obj, int len)
676
696
  case LOAD_GLOBAL:
677
697
  {
678
698
  int j = oparg;
699
+ next_stack++;
679
700
  if (j & 1) {
680
701
  next_stack++;
681
702
  }
682
- next_stack++;
683
703
  stacks[next_i] = next_stack;
684
704
  break;
685
705
  }
@@ -725,6 +745,7 @@ _ch_mark_stacks(PyCodeObject *code_obj, int len)
725
745
  stacks[next_i] = next_stack;
726
746
  }
727
747
  }
748
+ enabled_tracing[next_i] |= trace_enabled_here;
728
749
  i = next_i;
729
750
  }
730
751
  /* Scan exception table */
@@ -759,5 +780,11 @@ _ch_mark_stacks(PyCodeObject *code_obj, int len)
759
780
  }
760
781
  }
761
782
  Py_DECREF(co_code);
783
+ for (i = 0; i < len; i++) {
784
+ if (stacks[i] >= 0) {
785
+ stacks[i] = (stacks[i] << 1) | enabled_tracing[i];
786
+ }
787
+ }
788
+ PyMem_Free(enabled_tracing);
762
789
  return stacks;
763
790
  }
crosshair/_tracers.h CHANGED
@@ -50,10 +50,11 @@ typedef struct HandlerTable {
50
50
  } HandlerTable;
51
51
 
52
52
 
53
- typedef struct FrameAndCallback {
54
- PyObject* frame;
53
+ typedef struct FrameNextIandCallback {
54
+ PyFrameObject* frame;
55
+ int expected_i;
55
56
  PyObject* callback;
56
- } FrameAndCallback;
57
+ } FrameNextIandCallback;
57
58
 
58
59
 
59
60
  typedef struct CodeAndStacks {
@@ -62,7 +63,7 @@ typedef struct CodeAndStacks {
62
63
  } CodeAndStacks;
63
64
 
64
65
 
65
- DEFINE_VEC(FrameAndCallbackVec, FrameAndCallback, init_framecbvec, push_framecb);
66
+ DEFINE_VEC(FrameNextIandCallbackVec, FrameNextIandCallback, init_framecbvec, push_framecb);
66
67
  DEFINE_VEC(ModuleVec, PyObject*, init_modulevec, push_module);
67
68
  DEFINE_VEC(TableVec, HandlerTable, init_tablevec, push_table_entry)
68
69
 
@@ -70,9 +71,10 @@ typedef struct CTracer {
70
71
  PyObject_HEAD
71
72
  ModuleVec modules;
72
73
  TableVec handlers;
73
- FrameAndCallbackVec postop_callbacks;
74
+ FrameNextIandCallbackVec postop_callbacks;
74
75
  BOOL enabled;
75
76
  BOOL handling;
77
+ BOOL trace_all_opcodes;
76
78
  int thread_id;
77
79
  } CTracer;
78
80
 
@@ -87,4 +89,6 @@ typedef struct TraceSwap {
87
89
 
88
90
  extern PyTypeObject TraceSwapType;
89
91
 
92
+ extern const uint8_t _ch_TRACABLE_INSTRUCTIONS[256];
93
+
90
94
  #endif /* _COVERAGE_TRACER_H */
@@ -5,7 +5,11 @@ from typing import List
5
5
 
6
6
  import pytest
7
7
 
8
- from _crosshair_tracers import CTracer, code_stack_depths, frame_stack_read
8
+ from _crosshair_tracers import ( # type: ignore
9
+ CTracer,
10
+ code_stack_depths,
11
+ frame_stack_read,
12
+ )
9
13
  from crosshair.util import mem_usage_kb
10
14
 
11
15
 
@@ -15,19 +19,19 @@ class ExampleModule:
15
19
 
16
20
  def test_CTracer_module_refcounts_dont_leak():
17
21
  mod = ExampleModule()
18
- assert sys.getrefcount(mod) == 2
22
+ base_count = sys.getrefcount(mod)
19
23
  tracer = CTracer()
20
24
  tracer.push_module(mod)
21
- assert sys.getrefcount(mod) == 3
25
+ assert sys.getrefcount(mod) == base_count + 1
22
26
  tracer.push_module(mod)
23
27
  tracer.start()
24
28
  tracer.stop()
25
- assert sys.getrefcount(mod) == 4
29
+ assert sys.getrefcount(mod) == base_count + 2
26
30
  tracer.pop_module(mod)
27
- assert sys.getrefcount(mod) == 3
31
+ assert sys.getrefcount(mod) == base_count + 1
28
32
  del tracer
29
33
  gc.collect()
30
- assert sys.getrefcount(mod) == 2
34
+ assert sys.getrefcount(mod) == base_count
31
35
 
32
36
 
33
37
  def _get_depths(fn):
@@ -85,7 +89,10 @@ def _log_execution_stacks(fn, *a, **kw):
85
89
  return stacks
86
90
 
87
91
 
88
- @pytest.mark.skipif(sys.version_info < (3, 12), reason="stack depth on 3.12+")
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
+ )
89
96
  def test_one_function_stack_depth():
90
97
  _E = (TypeError, KeyboardInterrupt)
91
98
 
@@ -96,7 +103,10 @@ def test_one_function_stack_depth():
96
103
  _log_execution_stacks(a, 4)
97
104
 
98
105
 
99
- @pytest.mark.skipif(sys.version_info < (3, 12), reason="stack depth on 3.12+")
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
+ )
100
110
  def test_stack_get():
101
111
  def to_be_traced(x):
102
112
  r = 8 - x
@@ -125,4 +135,4 @@ def test_CTracer_does_not_leak_memory():
125
135
  if i == 100:
126
136
  usage = mem_usage_kb()
127
137
  usage_increase = mem_usage_kb() - usage
128
- assert usage_increase < 35
138
+ assert usage_increase < 200
crosshair/auditwall.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import importlib
2
+ import inspect
2
3
  import os
3
4
  import sys
4
5
  import traceback
@@ -28,9 +29,10 @@ def reject(event: str, args: Tuple) -> None:
28
29
 
29
30
 
30
31
  def inside_module(modules: Iterable[ModuleType]) -> bool:
31
- files = {m.__file__ for m in modules}
32
- for frame, lineno in traceback.walk_stack(None):
33
- if frame.f_code.co_filename in files:
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:
34
36
  return True
35
37
  return False
36
38
 
@@ -60,7 +62,7 @@ def check_msvcrt_open(event: str, args: Tuple) -> None:
60
62
  _MODULES_THAT_CAN_POPEN: Optional[Set[ModuleType]] = None
61
63
 
62
64
 
63
- def modules_with_allowed_popen():
65
+ def modules_with_allowed_subprocess():
64
66
  global _MODULES_THAT_CAN_POPEN
65
67
  if _MODULES_THAT_CAN_POPEN is None:
66
68
  allowed_module_names = ("_aix_support", "ctypes", "platform", "uuid")
@@ -74,13 +76,14 @@ def modules_with_allowed_popen():
74
76
 
75
77
 
76
78
  def check_subprocess(event: str, args: Tuple) -> None:
77
- if not inside_module(modules_with_allowed_popen()):
79
+ if not inside_module(modules_with_allowed_subprocess()):
78
80
  reject(event, args)
79
81
 
80
82
 
81
83
  _SPECIAL_HANDLERS = {
82
84
  "open": check_open,
83
85
  "subprocess.Popen": check_subprocess,
86
+ "os.posix_spawn": check_subprocess,
84
87
  "msvcrt.open_osfhandle": check_msvcrt_open,
85
88
  }
86
89
 
@@ -135,7 +138,6 @@ def make_handler(event: str) -> Callable[[str, Tuple], None]:
135
138
  "imaplib",
136
139
  "msvcrt",
137
140
  "nntplib",
138
- "os",
139
141
  "pathlib",
140
142
  "poplib",
141
143
  "shutil",
@@ -179,8 +181,7 @@ def opened_auditwall() -> Generator:
179
181
 
180
182
  def engage_auditwall() -> None:
181
183
  sys.dont_write_bytecode = True # disable .pyc file writing
182
- if sys.version_info >= (3, 8): # audithook is new in 3.8
183
- sys.addaudithook(audithook)
184
+ sys.addaudithook(audithook)
184
185
 
185
186
 
186
187
  def disable_auditwall() -> None:
@@ -11,33 +11,44 @@ from crosshair.auditwall import SideEffectDetected, engage_auditwall
11
11
  # audit hooks cannot be uninstalled, and we don't want to wall off the
12
12
  # testing process. Spawn subprcoesses instead.
13
13
 
14
- if sys.version_info >= (3, 8): # audithook is new in 3.8
15
- pyexec = sys.executable
14
+ pyexec = sys.executable
16
15
 
17
- def test_fs_read_allowed():
18
- assert call([pyexec, __file__, "read_open", "withwall"]) != 10
19
16
 
20
- def test_scandir_allowed():
21
- assert call([pyexec, __file__, "scandir", "withwall"]) == 0
17
+ def test_fs_read_allowed():
18
+ assert call([pyexec, __file__, "read_open", "withwall"]) != 10
22
19
 
23
- def test_import_allowed():
24
- assert call([pyexec, __file__, "import", "withwall"]) == 0
25
20
 
26
- def test_fs_write_disallowed():
27
- assert call([pyexec, __file__, "write_open", "withwall"]) == 10
21
+ def test_scandir_allowed():
22
+ assert call([pyexec, __file__, "scandir", "withwall"]) == 0
28
23
 
29
- def test_http_disallowed():
30
- assert call([pyexec, __file__, "http", "withwall"]) == 10
31
24
 
32
- def test_unlink_disallowed():
33
- assert call([pyexec, __file__, "unlink", "withwall"]) == 10
25
+ def test_import_allowed():
26
+ assert call([pyexec, __file__, "import", "withwall"]) == 0
34
27
 
35
- def test_popen_disallowed():
36
- assert call([pyexec, __file__, "popen", "withwall"]) == 10
37
28
 
38
- @pytest.mark.skipif(sys.version_info < (3, 9), reason="Python 3.9+ required")
39
- def test_popen_via_platform_allowed():
40
- assert call([pyexec, __file__, "popen_via_platform", "withwall"]) == 0
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
41
52
 
42
53
 
43
54
  _ACTIONS = {
@@ -51,6 +62,7 @@ _ACTIONS = {
51
62
  "popen_via_platform": lambda: platform._syscmd_ver( # type: ignore
52
63
  supported_platforms=(sys.platform,)
53
64
  ),
65
+ "chdir": lambda: os.chdir("."),
54
66
  }
55
67
 
56
68
  if __name__ == "__main__":
crosshair/codeconfig.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Configure analysis options at different levels."""
2
+
2
3
  import importlib.resources
3
4
  import inspect
4
5
  import re
@@ -25,7 +26,7 @@ def get_directives(source_text: str) -> Iterable[Tuple[int, int, str]]:
25
26
  ret = []
26
27
  tokens = tokenize.generate_tokens(StringIO(source_text).readline)
27
28
  # TODO catch tokenize.TokenError ... just in case?
28
- for (toktyp, tokval, begin, _, _) in tokens:
29
+ for toktyp, tokval, begin, _, _ in tokens:
29
30
  linenum, colnum = begin
30
31
  if toktyp == tokenize.COMMENT:
31
32
  directive = _COMMENT_TOKEN_RE.sub(r"\1", tokval)
@@ -39,7 +40,7 @@ class InvalidDirective(Exception):
39
40
 
40
41
 
41
42
  def parse_directives(
42
- directive_lines: Iterable[Tuple[int, int, str]]
43
+ directive_lines: Iterable[Tuple[int, int, str]],
43
44
  ) -> AnalysisOptionSet:
44
45
  """
45
46
  Parse options from directives in comments.