codesuture 0.5.1__tar.gz → 0.6.0__tar.gz

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 (52) hide show
  1. {codesuture-0.5.1 → codesuture-0.6.0}/.gitignore +1 -0
  2. {codesuture-0.5.1 → codesuture-0.6.0}/CHANGELOG.md +15 -0
  3. {codesuture-0.5.1 → codesuture-0.6.0}/PKG-INFO +2 -2
  4. {codesuture-0.5.1 → codesuture-0.6.0}/README.md +1 -1
  5. codesuture-0.6.0/codesuture/__init__.py +2 -0
  6. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/cli.py +3 -3
  7. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/guard_synthesizer.py +92 -1
  8. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/tracer.py +42 -11
  9. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/PKG-INFO +2 -2
  10. {codesuture-0.5.1 → codesuture-0.6.0}/pyproject.toml +2 -1
  11. codesuture-0.5.1/codesuture/__init__.py +0 -1
  12. {codesuture-0.5.1 → codesuture-0.6.0}/LICENSE +0 -0
  13. {codesuture-0.5.1 → codesuture-0.6.0}/MANIFEST.in +0 -0
  14. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/__main__.py +0 -0
  15. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/_eval_fix.py +0 -0
  16. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/audit.py +0 -0
  17. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/code_replacer.py +0 -0
  18. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/codesuture_fix.py +0 -0
  19. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/debuggee.py +0 -0
  20. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/diff_guard.py +0 -0
  21. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/explain.py +0 -0
  22. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/fingerprint.py +0 -0
  23. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/knowledge.py +0 -0
  24. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/middleware.py +0 -0
  25. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/pattern_matcher.py +0 -0
  26. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/persistence.py +0 -0
  27. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/plugins/__init__.py +0 -0
  28. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/plugins/autonomous.py +0 -0
  29. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/rewind.py +0 -0
  30. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/rollback.py +0 -0
  31. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/sandbox.py +0 -0
  32. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/shadow.py +0 -0
  33. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/watcher.py +0 -0
  34. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/SOURCES.txt +0 -0
  35. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/dependency_links.txt +0 -0
  36. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/entry_points.txt +0 -0
  37. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/requires.txt +0 -0
  38. {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/top_level.txt +0 -0
  39. {codesuture-0.5.1 → codesuture-0.6.0}/setup.cfg +0 -0
  40. {codesuture-0.5.1 → codesuture-0.6.0}/setup.py +0 -0
  41. {codesuture-0.5.1 → codesuture-0.6.0}/tests/__init__.py +0 -0
  42. {codesuture-0.5.1 → codesuture-0.6.0}/tests/closure_test.py +0 -0
  43. {codesuture-0.5.1 → codesuture-0.6.0}/tests/debug_gc.py +0 -0
  44. {codesuture-0.5.1 → codesuture-0.6.0}/tests/harness3.py +0 -0
  45. {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_codesuture_debuggee.py +0 -0
  46. {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_e2e.py +0 -0
  47. {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_guard_synthesizer.py +0 -0
  48. {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_harness.py +0 -0
  49. {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_harness2.py +0 -0
  50. {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_new_guards.py +0 -0
  51. {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_pattern_matcher.py +0 -0
  52. {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_unknown_bug.py +0 -0
@@ -25,6 +25,7 @@ realbugg.py
25
25
  real.py
26
26
  *.zip
27
27
  .livepatch_*/
28
+ v4test/
28
29
 
29
30
  # IDE / OS
30
31
  .vscode/
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-05-12
9
+
10
+ ### Fixed
11
+ - PEP 659: Force de-specialization after `__code__` swap via
12
+ `ctypes.pythonapi.PyFunction_SetCode` — prevents CPython 3.11+
13
+ adaptive bytecode cache from ignoring injected patches
14
+ - Thread blindness: Install trace hook on all threads via
15
+ `threading.settrace` at startup, with `_install_trace_on_all_threads`
16
+ helper covering existing and future threads; added `threading.Lock`
17
+ for thread-safe patch store writes
18
+ - Exception table corruption: Guard injection now detects try/except
19
+ scope via `TryBegin`/`TryEnd` markers and redirects to function
20
+ entry-point injection to avoid corrupting `co_exceptiontable` offsets
21
+ in CPython 3.11+
22
+
8
23
  ## [0.5.1] - 2026-05-11
9
24
 
10
25
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codesuture
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Runtime Python bytecode patcher with guard knowledge base, persistence, and self-healing re-execution
5
5
  License-Expression: MIT
6
6
  Project-URL: Source, https://github.com/codesuture-py/codesuture
@@ -158,7 +158,7 @@ Beyond basic patching, CodeSuture includes a set of higher-order behaviors that
158
158
 
159
159
  **Single-process scope.** Patches apply per-process. Multi-process applications need a CodeSuture instance per worker. The `.codesuture_store/` directory is shared on disk, so patches load correctly on restart.
160
160
 
161
- **Async support is experimental.** `async def` functions with standard `CO_COROUTINE` frames are patched. Async generators and deeply nested `await` chains may not be handled correctly in all cases.
161
+ **Web server support requires Python 3.11+ with `threading.settrace` active.** Tested with `socketserver` and `threading.Thread`. ASGI frameworks (FastAPI, Starlette) require the ASGI middleware wrapper.
162
162
 
163
163
  ---
164
164
 
@@ -137,7 +137,7 @@ Beyond basic patching, CodeSuture includes a set of higher-order behaviors that
137
137
 
138
138
  **Single-process scope.** Patches apply per-process. Multi-process applications need a CodeSuture instance per worker. The `.codesuture_store/` directory is shared on disk, so patches load correctly on restart.
139
139
 
140
- **Async support is experimental.** `async def` functions with standard `CO_COROUTINE` frames are patched. Async generators and deeply nested `await` chains may not be handled correctly in all cases.
140
+ **Web server support requires Python 3.11+ with `threading.settrace` active.** Tested with `socketserver` and `threading.Thread`. ASGI frameworks (FastAPI, Starlette) require the ASGI middleware wrapper.
141
141
 
142
142
  ---
143
143
 
@@ -0,0 +1,2 @@
1
+ __version__ = "0.6.0"
2
+
@@ -1,11 +1,11 @@
1
1
  import sys
2
2
  import argparse
3
- from codesuture.tracer import install, uninstall
3
+ from codesuture.tracer import install, uninstall, _install_trace_on_all_threads
4
4
 
5
5
  def main():
6
6
  parser = argparse.ArgumentParser(prog='codesuture',
7
7
  description='Runtime Python bytecode patcher with self-healing re-execution')
8
- parser.add_argument('--version', action='version', version='codesuture 0.5.1')
8
+ parser.add_argument('--version', action='version', version='codesuture 0.6.0')
9
9
  sub = parser.add_subparsers(dest='command', required=True)
10
10
 
11
11
  run_parser = sub.add_parser('run', help='Run a script with live patching')
@@ -102,7 +102,7 @@ def main():
102
102
  patched_before = tracer.stats['patched']
103
103
  tracer._handled_exc_ids.clear()
104
104
  try:
105
- sys.settrace(tracer)
105
+ _install_trace_on_all_threads(tracer)
106
106
  globs = make_persisted_patch_globals(
107
107
  "__main__",
108
108
  {'__name__': '__main__', '__file__': args.script},
@@ -1,9 +1,30 @@
1
1
  """
2
2
  Synthesises guard + original bytecode for all deterministic strategies.
3
3
  """
4
+ import ctypes
4
5
  from bytecode import Bytecode, Instr, Label, Compare
5
6
  from codesuture.pattern_matcher import PatchSpec
6
7
 
8
+ def _force_despecialize(func):
9
+ """
10
+ Force CPython 3.11+ to abandon its adaptive
11
+ instruction cache for this function.
12
+ After __code__ replacement, the interpreter must
13
+ re-read the new bytecode from scratch.
14
+ """
15
+ try:
16
+ # PyFunction_SetCode forces de-specialization
17
+ # by going through the official C API path
18
+ # rather than the Python attribute setter.
19
+ ctypes.pythonapi.PyFunction_SetCode(
20
+ ctypes.py_object(func),
21
+ ctypes.py_object(func.__code__)
22
+ )
23
+ except Exception:
24
+ pass # Non-fatal: patch still applied, may not
25
+ # take effect until next function cold-start
26
+
27
+
7
28
  class PatchValidationError(Exception):
8
29
  pass
9
30
 
@@ -51,21 +72,91 @@ def propagate_patch(original_func, patched_code) -> int:
51
72
  if hasattr(ref, '__func__') and hasattr(ref.__func__, '__code__'):
52
73
  if ref.__func__.__code__ is original_code:
53
74
  ref.__func__.__code__ = patched_code
75
+ _force_despecialize(ref.__func__)
54
76
  propagated += 1
55
77
 
56
78
  elif hasattr(ref, '__code__') and ref.__code__ is original_code:
57
79
  ref.__code__ = patched_code
80
+ _force_despecialize(ref)
58
81
  propagated += 1
59
82
 
60
83
  original_func.__code__ = patched_code
84
+ _force_despecialize(original_func)
61
85
 
62
86
  if propagated > 0:
63
87
  print(f"[CodeSuture] Propagated patch to {propagated} additional "
64
88
  f"live reference(s) of {original_func.__qualname__}.")
65
89
  return propagated
66
90
 
91
+ def _is_inside_try_block(code):
92
+ """Return True if any BINARY_SUBSCR or crash-relevant opcode
93
+ falls inside an exception handler range (TryBegin/TryEnd).
94
+ Uses the bytecode library's TryBegin/TryEnd markers."""
95
+ import sys
96
+ if sys.version_info < (3, 11):
97
+ return False
98
+ try:
99
+ from bytecode import TryBegin, TryEnd
100
+ bc = Bytecode.from_code(code)
101
+ depth = 0
102
+ has_subscr_in_try = False
103
+ for item in bc:
104
+ if isinstance(item, TryBegin):
105
+ depth += 1
106
+ elif isinstance(item, TryEnd):
107
+ depth = max(0, depth - 1)
108
+ elif depth > 0 and isinstance(item, Instr):
109
+ if item.name in ('BINARY_SUBSCR', 'LOAD_ATTR', 'LOAD_METHOD',
110
+ 'BINARY_OP', 'BINARY_TRUE_DIVIDE'):
111
+ has_subscr_in_try = True
112
+ break
113
+ return has_subscr_in_try
114
+ except Exception:
115
+ return False
116
+
117
+ def _build_entry_point_null_guard(original_code, var_name, default):
118
+ """Build a guard injected at the function entry point (after RESUME).
119
+ Checks if var_name is None and replaces it with default.
120
+ Safe for use when the crash site is inside a try block."""
121
+ bc = Bytecode.from_code(original_code)
122
+ skip = Label()
123
+ patch = [
124
+ Instr('LOAD_FAST', var_name),
125
+ Instr('LOAD_CONST', None),
126
+ Instr('IS_OP', 0),
127
+ Instr('POP_JUMP_FORWARD_IF_FALSE', skip),
128
+ Instr('LOAD_CONST', default),
129
+ Instr('RETURN_VALUE'),
130
+ skip
131
+ ]
132
+ idx = 0
133
+ for i, instr in enumerate(bc):
134
+ if isinstance(instr, Instr) and instr.name == 'RESUME':
135
+ idx = i + 1
136
+ break
137
+ for instr in reversed(patch):
138
+ bc.insert(idx, instr)
139
+ return bc
140
+
141
+ # Strategies that inject inline at the crash site (replace BINARY_SUBSCR etc.)
142
+ # These are the ones that can corrupt exception tables when inside try blocks.
143
+ _INLINE_STRATEGIES = frozenset({
144
+ 'subscript_guard', 'key_guard', 'dict_get_guard',
145
+ 'chain_subscript_guard', 'division_guard',
146
+ })
147
+
67
148
  def synthesize_guarded_code(original_code, spec: PatchSpec) -> Bytecode:
68
- if spec.strategy in ('subscript_guard', 'key_guard', 'dict_get_guard'):
149
+ # Bug 3 fix: If the crash site is inside a try/except block and we
150
+ # would normally inject inline, redirect to an entry-point guard
151
+ # to avoid corrupting co_exceptiontable offsets on CPython 3.11+.
152
+ if spec.strategy in _INLINE_STRATEGIES and _is_inside_try_block(original_code):
153
+ import logging
154
+ logging.getLogger(__name__).debug(
155
+ "[CodeSuture] Crash inside try block — redirecting %s to entry-point guard",
156
+ spec.strategy
157
+ )
158
+ res = _build_entry_point_null_guard(original_code, spec.var_name, spec.default_value)
159
+ elif spec.strategy in ('subscript_guard', 'key_guard', 'dict_get_guard'):
69
160
  res = _build_subscript_guarded_code(original_code, spec.var_name, spec.key_name, spec.default_value)
70
161
  elif spec.strategy == 'chain_subscript_guard':
71
162
  res = _build_chain_subscript_guarded_code(original_code, spec.var_name, spec.key_name, spec.default_value)
@@ -3,7 +3,7 @@ import os
3
3
  import json
4
4
  from datetime import datetime
5
5
  from codesuture.pattern_matcher import analyze_exception
6
- from codesuture.guard_synthesizer import synthesize_guarded_code
6
+ from codesuture.guard_synthesizer import synthesize_guarded_code, _force_despecialize
7
7
  from codesuture.code_replacer import replace_function_code, get_function_from_frame
8
8
  from codesuture.rewind import rewind_frame_to_start
9
9
 
@@ -27,6 +27,7 @@ def _is_internal_frame(frame):
27
27
 
28
28
  class CodeSutureTracer:
29
29
  def __init__(self, dry_run=False, log_file=None, max_retries=3, autonomous=False, script_path=None, verbose=False, shadow=False, ttl=7):
30
+ import threading
30
31
  self.dry_run = dry_run
31
32
  self.log_file = log_file
32
33
  self.max_retries = max_retries
@@ -44,6 +45,7 @@ class CodeSutureTracer:
44
45
  }
45
46
  self.patched_signatures = {}
46
47
  self._handled_exc_ids = set()
48
+ self._patch_lock = threading.Lock()
47
49
 
48
50
  def __call__(self, frame, event, arg):
49
51
  if event == 'return' and self.shadow_mode and frame.f_code in self._patched_codes:
@@ -271,22 +273,24 @@ class CodeSutureTracer:
271
273
  if getattr(spec, 'target_func', None):
272
274
  assert spec.target_func.__code__ is new_code, "Property fget code replacement failed"
273
275
 
274
- from codesuture.persistence import save_patch
275
- save_patch(func, new_code, spec, self.ttl)
276
+ with self._patch_lock:
277
+ from codesuture.persistence import save_patch
278
+ save_patch(func, new_code, spec, self.ttl)
276
279
 
277
- if self.shadow_mode:
278
- self._patched_codes[new_code] = spec.strategy
280
+ if self.shadow_mode:
281
+ self._patched_codes[new_code] = spec.strategy
279
282
 
280
283
  if self.verbose:
281
284
  from codesuture.diff_guard import semantic_diff
282
285
  diff = semantic_diff(old_code, new_code, spec.strategy)
283
286
  print(f"[CodeSuture DEBUG] Diff: +{diff.added} -{diff.removed} instructions (allowed <= {diff.allowed})")
284
287
 
285
- if not is_reuse:
286
- self.patched_signatures[sig] = spec
288
+ with self._patch_lock:
289
+ if not is_reuse:
290
+ self.patched_signatures[sig] = spec
287
291
 
288
- if not cached:
289
- record(fp, spec.strategy, spec.var_name, getattr(func, '__name__', 'unknown'), exc_type.__name__, spec.default_value, spec.key_name)
292
+ if not cached:
293
+ record(fp, spec.strategy, spec.var_name, getattr(func, '__name__', 'unknown'), exc_type.__name__, spec.default_value, spec.key_name)
290
294
 
291
295
  entry["action"] = "applied"
292
296
  self._log(entry)
@@ -347,6 +351,7 @@ class CodeSutureTracer:
347
351
  new_bc = synthesize_guarded_code(internal_frame.f_code, spec)
348
352
  new_code = new_bc.to_code()
349
353
  replace_function_code(func, new_code)
354
+ _force_despecialize(func)
350
355
  self.stats["patched"] += 1
351
356
  print(f"[CodeSuture] Self-healed {func.__name__}().")
352
357
 
@@ -369,6 +374,7 @@ class CodeSutureTracer:
369
374
  if hasattr(ref, "__code__") and getattr(ref, "__code__", None) is old_code:
370
375
  try:
371
376
  ref.__code__ = new_code
377
+ _force_despecialize(ref)
372
378
  replaced = True
373
379
  propagated_count += 1
374
380
  except Exception:
@@ -378,6 +384,7 @@ class CodeSutureTracer:
378
384
  if hasattr(fn, "__code__") and getattr(fn, "__code__", None) is old_code:
379
385
  try:
380
386
  fn.__code__ = new_code
387
+ _force_despecialize(fn)
381
388
  replaced = True
382
389
  propagated_count += 1
383
390
  except Exception:
@@ -404,6 +411,7 @@ class CodeSutureTracer:
404
411
 
405
412
  if func and hasattr(func, "__code__") and getattr(func, "__code__", None) is old_code:
406
413
  func.__code__ = new_code
414
+ _force_despecialize(func)
407
415
  print(f"[CodeSuture] In-memory propagated patch applied to {func.__name__}().")
408
416
  else:
409
417
  print("[CodeSuture] Could not find code object in memory to persist.")
@@ -428,17 +436,40 @@ def _codesuture_excepthook(tracer, exc_type, exc_value, exc_tb):
428
436
  if exc_tb:
429
437
  tracer._handle_exception(exc_tb.tb_frame, exc_type, exc_value, exc_tb, thread=threading.current_thread())
430
438
 
439
+ # If CodeSuture handled and patched this exception, suppress the
440
+ # default traceback — the patch was applied and re-execution will
441
+ # handle the rest.
442
+ if id(exc_value) in tracer._handled_exc_ids:
443
+ return
444
+
431
445
  if _original_excepthook:
432
446
  _original_excepthook(exc_type, exc_value, exc_tb)
433
447
  else:
434
448
  sys.__excepthook__(exc_type, exc_value, exc_tb)
435
449
 
450
+ def _install_trace_on_all_threads(trace_fn):
451
+ """Install trace hook on main thread and
452
+ all currently running threads."""
453
+ import threading
454
+ sys.settrace(trace_fn)
455
+ threading.settrace(trace_fn)
456
+ # For threads already running:
457
+ for thread in threading.enumerate():
458
+ if thread is not threading.current_thread():
459
+ try:
460
+ import ctypes
461
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(
462
+ ctypes.c_ulong(thread.ident),
463
+ None # does not raise, just wakes thread
464
+ )
465
+ except Exception:
466
+ pass
467
+
436
468
  def install(dry_run=False, log_file=None, max_retries=3, autonomous=False, script_path=None, verbose=False, shadow=False, ttl=7):
437
469
  global _original_excepthook
438
470
  import threading
439
471
  tracer = CodeSutureTracer(dry_run, log_file, max_retries, autonomous, script_path, verbose, shadow, ttl)
440
- sys.settrace(tracer)
441
- threading.settrace(tracer)
472
+ _install_trace_on_all_threads(tracer)
442
473
 
443
474
  if getattr(threading, 'excepthook', None) is not None:
444
475
  if threading.excepthook != getattr(threading, '__excepthook__', None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codesuture
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Runtime Python bytecode patcher with guard knowledge base, persistence, and self-healing re-execution
5
5
  License-Expression: MIT
6
6
  Project-URL: Source, https://github.com/codesuture-py/codesuture
@@ -158,7 +158,7 @@ Beyond basic patching, CodeSuture includes a set of higher-order behaviors that
158
158
 
159
159
  **Single-process scope.** Patches apply per-process. Multi-process applications need a CodeSuture instance per worker. The `.codesuture_store/` directory is shared on disk, so patches load correctly on restart.
160
160
 
161
- **Async support is experimental.** `async def` functions with standard `CO_COROUTINE` frames are patched. Async generators and deeply nested `await` chains may not be handled correctly in all cases.
161
+ **Web server support requires Python 3.11+ with `threading.settrace` active.** Tested with `socketserver` and `threading.Thread`. ASGI frameworks (FastAPI, Starlette) require the ASGI middleware wrapper.
162
162
 
163
163
  ---
164
164
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codesuture"
7
- version = "0.5.1"
7
+ version = "0.6.0"
8
8
  description = "Runtime Python bytecode patcher with guard knowledge base, persistence, and self-healing re-execution"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -35,3 +35,4 @@ codesuture = "codesuture.cli:main"
35
35
  [tool.setuptools.packages.find]
36
36
  include = ["codesuture*"]
37
37
 
38
+
@@ -1 +0,0 @@
1
- __version__ = "0.5.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes