codesuture 0.5.0__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.
- codesuture-0.6.0/.gitignore +33 -0
- codesuture-0.6.0/CHANGELOG.md +71 -0
- codesuture-0.6.0/MANIFEST.in +14 -0
- codesuture-0.6.0/PKG-INFO +167 -0
- codesuture-0.6.0/README.md +146 -0
- codesuture-0.6.0/codesuture/__init__.py +2 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/cli.py +3 -3
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/guard_synthesizer.py +109 -2
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/pattern_matcher.py +58 -7
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/tracer.py +52 -11
- codesuture-0.6.0/codesuture.egg-info/PKG-INFO +167 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture.egg-info/SOURCES.txt +7 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/pyproject.toml +2 -1
- codesuture-0.6.0/tests/__init__.py +0 -0
- codesuture-0.6.0/tests/closure_test.py +16 -0
- codesuture-0.6.0/tests/debug_gc.py +23 -0
- codesuture-0.6.0/tests/harness3.py +46 -0
- codesuture-0.5.0/PKG-INFO +0 -106
- codesuture-0.5.0/README.md +0 -85
- codesuture-0.5.0/codesuture/__init__.py +0 -1
- codesuture-0.5.0/codesuture.egg-info/PKG-INFO +0 -106
- {codesuture-0.5.0 → codesuture-0.6.0}/LICENSE +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/__main__.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/_eval_fix.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/audit.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/code_replacer.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/codesuture_fix.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/debuggee.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/diff_guard.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/explain.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/fingerprint.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/knowledge.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/middleware.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/persistence.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/plugins/__init__.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/plugins/autonomous.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/rewind.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/rollback.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/sandbox.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/shadow.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture/watcher.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture.egg-info/dependency_links.txt +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture.egg-info/entry_points.txt +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture.egg-info/requires.txt +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/codesuture.egg-info/top_level.txt +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/setup.cfg +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/setup.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/tests/test_codesuture_debuggee.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/tests/test_e2e.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/tests/test_guard_synthesizer.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/tests/test_harness.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/tests/test_harness2.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/tests/test_new_guards.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/tests/test_pattern_matcher.py +0 -0
- {codesuture-0.5.0 → codesuture-0.6.0}/tests/test_unknown_bug.py +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
|
|
14
|
+
# CodeSuture specific
|
|
15
|
+
.codesuture_cache/
|
|
16
|
+
.codesuture_store/
|
|
17
|
+
.codesuture_knowledge/
|
|
18
|
+
.codesuture_fingerprints
|
|
19
|
+
.models/
|
|
20
|
+
|
|
21
|
+
# Exclude internal / dev scripts
|
|
22
|
+
codesuture_verify*.py
|
|
23
|
+
livepatch_verify*.py
|
|
24
|
+
realbugg.py
|
|
25
|
+
real.py
|
|
26
|
+
*.zip
|
|
27
|
+
.livepatch_*/
|
|
28
|
+
v4test/
|
|
29
|
+
|
|
30
|
+
# IDE / OS
|
|
31
|
+
.vscode/
|
|
32
|
+
.idea/
|
|
33
|
+
.DS_Store
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
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
|
+
|
|
23
|
+
## [0.5.1] - 2026-05-11
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- propagate_patch: skip list/dict/set/generator comprehensions
|
|
27
|
+
instead of crashing with AttributeError on __code__
|
|
28
|
+
- key_guard, subscript_guard, chain_subscript_guard: infer
|
|
29
|
+
correct default type from downstream bytecode usage
|
|
30
|
+
(string methods -> "" default, numeric ops -> 0 default)
|
|
31
|
+
- KeyError on chained subscripts (e.g. request["headers"]["auth"].strip())
|
|
32
|
+
now produces a chain_subscript_guard instead of a simple key_guard,
|
|
33
|
+
preventing secondary TypeError from None subscript access
|
|
34
|
+
|
|
35
|
+
## [0.5.0] - 2026-05-08
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- Async/await support (CO_COROUTINE frame detection) — automatic `RESUME 0` preservation for coroutine bytecode patching.
|
|
39
|
+
- Watch mode: `codesuture watch --max-restarts N` — subprocess loop with automatic crash-patch-restart cycle.
|
|
40
|
+
- Explain command: `codesuture explain [func_name]` — detailed table of active patches with safety assessment (LIKELY/RISKY/UNKNOWN).
|
|
41
|
+
- WSGI middleware: `CodeSutureMiddleware` — intercepts request handler exceptions, patches, and retries with `X-CodeSuture` response header.
|
|
42
|
+
|
|
43
|
+
## [0.4.0] - 2026-05-07
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- `codesuture rollback` command to selectively remove persisted patches (`codesuture rollback <func>`, `--all`, and `--dry-run`).
|
|
47
|
+
- Three new guard types:
|
|
48
|
+
- `type_coercion_guard` for `TypeError` and `ValueError` during type conversions.
|
|
49
|
+
- `index_guard` for `IndexError` bounds checking.
|
|
50
|
+
- `key_guard` for safe dictionary `KeyError` fallbacks.
|
|
51
|
+
- Enhanced `--dry-run` mode with confidence levels (HIGH/MEDIUM/LOW) based on fingerprint registry hits.
|
|
52
|
+
- Full PyPI packaging structure (`pyproject.toml`, complete `README.md`, `CHANGELOG.md`).
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
- Migrated legacy guards `list_bound_guard` to `index_guard` and `dict_get_guard` to `key_guard` for consistency.
|
|
56
|
+
- Standardized CLI output format and improved error reporting.
|
|
57
|
+
|
|
58
|
+
## [0.3.0] - 2026-05-06
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
- Dark Upgrade D1: Semantic diff safety gate to prevent runaway bytecode corruption.
|
|
62
|
+
- Dark Upgrade D2: Caller-aware patch propagation to automatically fix closures and bound methods in-memory.
|
|
63
|
+
- Dark Upgrade D3: Shadow execution mode (`--shadow`) to monitor and warn when sentinel defaults leak downstream.
|
|
64
|
+
- Dark Upgrade D4: Patch expiry TTL warnings to nudge developers toward source-level fixes.
|
|
65
|
+
- Dark Upgrade D5: Bytecode fingerprint registry for instant cache hits on known crash patterns.
|
|
66
|
+
- Dark Upgrade D6: `codesuture audit` command for viewing all active patches in a formatted table.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
- Addressed Windows `UnicodeDecodeError` and `cp1252` terminal limitations by enforcing `utf-8` encoding.
|
|
70
|
+
- Resolved a race condition where patch persistence was executing after the code object swap, preventing correct caller identification.
|
|
71
|
+
- Fixed namespace pollution during nested patching.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
include pyproject.toml
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include LICENSE
|
|
5
|
+
include .gitignore
|
|
6
|
+
|
|
7
|
+
recursive-include codesuture *.py
|
|
8
|
+
recursive-include tests *.py
|
|
9
|
+
|
|
10
|
+
exclude codesuture_verify*.py
|
|
11
|
+
exclude livepatch_verify*.py
|
|
12
|
+
exclude realbugg.py
|
|
13
|
+
exclude real.py
|
|
14
|
+
exclude *.zip
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codesuture
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Runtime Python bytecode patcher with guard knowledge base, persistence, and self-healing re-execution
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Source, https://github.com/codesuture-py/codesuture
|
|
7
|
+
Keywords: bytecode,runtime,patching,self-healing,debugging,null-safety,resilience
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: bytecode>=0.15.1
|
|
18
|
+
Provides-Extra: autonomous
|
|
19
|
+
Requires-Dist: llama-cpp-python; extra == "autonomous"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# CodeSuture
|
|
23
|
+
|
|
24
|
+
> Runtime Python bytecode patcher. Catches crashes, synthesizes guards, rewrites functions in-memory, and persists fixes across runs.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## What it does
|
|
29
|
+
|
|
30
|
+
CodeSuture sits between your Python program and its crashes. When an exception occurs, it intercepts the failing frame, disassembles the bytecode to identify the root cause, injects a deterministic guard directly into the function's code object, and retries execution — all without touching a single source file.
|
|
31
|
+
|
|
32
|
+
The fix persists. On subsequent runs, CodeSuture loads the patched bytecode before the first function call. The crash is gone until you choose to remove the patch.
|
|
33
|
+
|
|
34
|
+
It is a surgical tool for keeping programs running when structural failures — null access, missing keys, type mismatches, out-of-bounds reads — would otherwise bring them down.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install codesuture
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Python 3.11+ required.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
codesuture run your_script.py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
[CodeSuture] Caught AttributeError: 'NoneType' object has no attribute 'bio'
|
|
56
|
+
[CodeSuture] Applying null_guard on 'profile' ...
|
|
57
|
+
[CodeSuture] Patch applied to get_bio().
|
|
58
|
+
[CodeSuture] Re-executing after 1 patch(es)...
|
|
59
|
+
|
|
60
|
+
Session summary:
|
|
61
|
+
Patches applied: 1
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run it again:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
[CodeSuture] Already healed, skipping: loaded persistent patch for get_bio
|
|
68
|
+
[CodeSuture] Session summary:
|
|
69
|
+
Patches applied: 0
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
1. **Catch** — A `sys.settrace` callback intercepts exceptions at the exact frame and bytecode offset where they occur.
|
|
77
|
+
2. **Analyze** — The pattern matcher disassembles the function's bytecode, walks the instruction chain, and identifies the failing variable or operation.
|
|
78
|
+
3. **Patch** — The guard synthesizer injects new bytecode instructions into the function's code object in memory. A semantic diff gate rejects patches that would change too much of the function's logic.
|
|
79
|
+
4. **Rewind** — Execution restarts from the patched function. The guard prevents the crash from recurring.
|
|
80
|
+
5. **Persist** — The patched code object is serialized to `.codesuture_store/` with JSON metadata. On subsequent runs it loads before execution begins.
|
|
81
|
+
|
|
82
|
+
No source files are modified at any point.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Supported guard types
|
|
87
|
+
|
|
88
|
+
| Guard type | Triggers on | Example |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `null_guard` | `AttributeError` on `None` | `user.profile.bio` when `profile` is `None` |
|
|
91
|
+
| `index_guard` | `IndexError` | `items[10]` when `len(items) == 2` |
|
|
92
|
+
| `key_guard` | `KeyError` | `cfg["timeout"]` when key is missing |
|
|
93
|
+
| `subscript_guard` | `TypeError` subscripting `None` | `data["key"]` when `data` is `None` |
|
|
94
|
+
| `chain_subscript_guard` | Nested subscript on `None` | `data["user"]["name"]` |
|
|
95
|
+
| `type_coercion_guard` | `TypeError` on conversion | `int("not_a_number")` |
|
|
96
|
+
| `division_guard` | `ZeroDivisionError` | `x / count` when `count == 0` |
|
|
97
|
+
| `str_coerce_guard` | `TypeError` on string concat | `"age: " + 25` |
|
|
98
|
+
| `file_guard` | `FileNotFoundError` | `open(path)` when file is missing |
|
|
99
|
+
| `callable_guard` | `TypeError` calling `None` | `func()` when `func` is `None` |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## CLI reference
|
|
104
|
+
|
|
105
|
+
| Command | Flags | What it does |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `codesuture run <script>` | | Run script with live patching enabled |
|
|
108
|
+
| `codesuture run <script>` | `--verbose` | Show patch diffs and instruction deltas |
|
|
109
|
+
| `codesuture run <script>` | `--shadow` | Warn when patched functions return sentinel values |
|
|
110
|
+
| `codesuture run <script>` | `--dry-run` | Preview what would be patched without applying |
|
|
111
|
+
| `codesuture run <script>` | `--ttl DAYS` | Set patch expiry in days (default: 7) |
|
|
112
|
+
| `codesuture run <script>` | `--retries N` | Max re-execution attempts per crash (default: 3) |
|
|
113
|
+
| `codesuture watch <script>` | `--max-restarts N` | Run continuously, restart after each patch |
|
|
114
|
+
| `codesuture audit` | | Show all active patches in a formatted table |
|
|
115
|
+
| `codesuture explain` | | Human-readable breakdown of what each patch changed |
|
|
116
|
+
| `codesuture explain <name>` | | Explain one function's patch |
|
|
117
|
+
| `codesuture rollback <name>` | | Remove one persisted patch |
|
|
118
|
+
| `codesuture rollback` | `--all` | Remove all patches and the fingerprint registry |
|
|
119
|
+
| `codesuture rollback` | `--dry-run` | Preview what would be removed |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Runtime Intelligence
|
|
124
|
+
|
|
125
|
+
Beyond basic patching, CodeSuture includes a set of higher-order behaviors that make it safe to use in real codebases:
|
|
126
|
+
|
|
127
|
+
**Semantic diff gate** — Patches that would modify too many instructions relative to the guard type are automatically rejected. The engine will never corrupt a complex function to patch a simple crash.
|
|
128
|
+
|
|
129
|
+
**Caller-aware propagation** — After patching a function, CodeSuture uses `gc.get_referrers` to find every live reference to the original code object — closures, bound methods, partials — and updates them all. No in-memory copy of the broken function survives.
|
|
130
|
+
|
|
131
|
+
**Shadow execution mode** — `--shadow` monitors the return value of patched functions. If a sentinel default (`""`, `0`, `None`) leaks into downstream logic, CodeSuture logs a warning before it causes a second failure.
|
|
132
|
+
|
|
133
|
+
**Patch expiry** — Every persisted patch carries a configurable TTL. When a patch ages past its limit, CodeSuture logs a reminder that the underlying bug should be addressed in source code. Patches are scaffolding, not permanent fixes.
|
|
134
|
+
|
|
135
|
+
**Bytecode fingerprint registry** — Crash sites are hashed by their surrounding instruction window. When the same crash pattern appears again, CodeSuture applies the known guard directly without re-running analysis.
|
|
136
|
+
|
|
137
|
+
**Audit trail** — `codesuture audit` displays every active patch: function name, guard type, target variable, default value, age, and rollback instructions. `codesuture explain` gives a plain-language breakdown of exactly what each patch changed and whether the default value is safe for downstream consumers.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## What CodeSuture is not
|
|
142
|
+
|
|
143
|
+
**Not a logger.** It does not record exceptions and move on. It patches the function and retries.
|
|
144
|
+
|
|
145
|
+
**Not a fuzzer or static analyzer.** It operates at runtime on live bytecode, not on source.
|
|
146
|
+
|
|
147
|
+
**Not autonomous.** Patches should be reviewed via `codesuture audit` and `codesuture explain`. The goal is to keep your program running while you address the root cause — not to replace the fix.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Limitations
|
|
152
|
+
|
|
153
|
+
**Python 3.11+ only.** CodeSuture depends on bytecode structures introduced in CPython 3.11. Earlier versions are not supported.
|
|
154
|
+
|
|
155
|
+
**List and dict comprehensions are not patchable.** Comprehensions are implemented as anonymous nested code objects in CPython. CodeSuture detects crashes inside them and logs a warning, but does not attempt to patch them. Refactor the comprehension into a named function to enable patching.
|
|
156
|
+
|
|
157
|
+
**Semantic bugs are not patchable.** CodeSuture fixes structural crashes — null access, missing keys, type mismatches, bounds errors. It cannot fix logic errors where the code runs but produces wrong results.
|
|
158
|
+
|
|
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
|
+
|
|
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
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# CodeSuture
|
|
2
|
+
|
|
3
|
+
> Runtime Python bytecode patcher. Catches crashes, synthesizes guards, rewrites functions in-memory, and persists fixes across runs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
CodeSuture sits between your Python program and its crashes. When an exception occurs, it intercepts the failing frame, disassembles the bytecode to identify the root cause, injects a deterministic guard directly into the function's code object, and retries execution — all without touching a single source file.
|
|
10
|
+
|
|
11
|
+
The fix persists. On subsequent runs, CodeSuture loads the patched bytecode before the first function call. The crash is gone until you choose to remove the patch.
|
|
12
|
+
|
|
13
|
+
It is a surgical tool for keeping programs running when structural failures — null access, missing keys, type mismatches, out-of-bounds reads — would otherwise bring them down.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install codesuture
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Python 3.11+ required.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
codesuture run your_script.py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
[CodeSuture] Caught AttributeError: 'NoneType' object has no attribute 'bio'
|
|
35
|
+
[CodeSuture] Applying null_guard on 'profile' ...
|
|
36
|
+
[CodeSuture] Patch applied to get_bio().
|
|
37
|
+
[CodeSuture] Re-executing after 1 patch(es)...
|
|
38
|
+
|
|
39
|
+
Session summary:
|
|
40
|
+
Patches applied: 1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run it again:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
[CodeSuture] Already healed, skipping: loaded persistent patch for get_bio
|
|
47
|
+
[CodeSuture] Session summary:
|
|
48
|
+
Patches applied: 0
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## How it works
|
|
54
|
+
|
|
55
|
+
1. **Catch** — A `sys.settrace` callback intercepts exceptions at the exact frame and bytecode offset where they occur.
|
|
56
|
+
2. **Analyze** — The pattern matcher disassembles the function's bytecode, walks the instruction chain, and identifies the failing variable or operation.
|
|
57
|
+
3. **Patch** — The guard synthesizer injects new bytecode instructions into the function's code object in memory. A semantic diff gate rejects patches that would change too much of the function's logic.
|
|
58
|
+
4. **Rewind** — Execution restarts from the patched function. The guard prevents the crash from recurring.
|
|
59
|
+
5. **Persist** — The patched code object is serialized to `.codesuture_store/` with JSON metadata. On subsequent runs it loads before execution begins.
|
|
60
|
+
|
|
61
|
+
No source files are modified at any point.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Supported guard types
|
|
66
|
+
|
|
67
|
+
| Guard type | Triggers on | Example |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `null_guard` | `AttributeError` on `None` | `user.profile.bio` when `profile` is `None` |
|
|
70
|
+
| `index_guard` | `IndexError` | `items[10]` when `len(items) == 2` |
|
|
71
|
+
| `key_guard` | `KeyError` | `cfg["timeout"]` when key is missing |
|
|
72
|
+
| `subscript_guard` | `TypeError` subscripting `None` | `data["key"]` when `data` is `None` |
|
|
73
|
+
| `chain_subscript_guard` | Nested subscript on `None` | `data["user"]["name"]` |
|
|
74
|
+
| `type_coercion_guard` | `TypeError` on conversion | `int("not_a_number")` |
|
|
75
|
+
| `division_guard` | `ZeroDivisionError` | `x / count` when `count == 0` |
|
|
76
|
+
| `str_coerce_guard` | `TypeError` on string concat | `"age: " + 25` |
|
|
77
|
+
| `file_guard` | `FileNotFoundError` | `open(path)` when file is missing |
|
|
78
|
+
| `callable_guard` | `TypeError` calling `None` | `func()` when `func` is `None` |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## CLI reference
|
|
83
|
+
|
|
84
|
+
| Command | Flags | What it does |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `codesuture run <script>` | | Run script with live patching enabled |
|
|
87
|
+
| `codesuture run <script>` | `--verbose` | Show patch diffs and instruction deltas |
|
|
88
|
+
| `codesuture run <script>` | `--shadow` | Warn when patched functions return sentinel values |
|
|
89
|
+
| `codesuture run <script>` | `--dry-run` | Preview what would be patched without applying |
|
|
90
|
+
| `codesuture run <script>` | `--ttl DAYS` | Set patch expiry in days (default: 7) |
|
|
91
|
+
| `codesuture run <script>` | `--retries N` | Max re-execution attempts per crash (default: 3) |
|
|
92
|
+
| `codesuture watch <script>` | `--max-restarts N` | Run continuously, restart after each patch |
|
|
93
|
+
| `codesuture audit` | | Show all active patches in a formatted table |
|
|
94
|
+
| `codesuture explain` | | Human-readable breakdown of what each patch changed |
|
|
95
|
+
| `codesuture explain <name>` | | Explain one function's patch |
|
|
96
|
+
| `codesuture rollback <name>` | | Remove one persisted patch |
|
|
97
|
+
| `codesuture rollback` | `--all` | Remove all patches and the fingerprint registry |
|
|
98
|
+
| `codesuture rollback` | `--dry-run` | Preview what would be removed |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Runtime Intelligence
|
|
103
|
+
|
|
104
|
+
Beyond basic patching, CodeSuture includes a set of higher-order behaviors that make it safe to use in real codebases:
|
|
105
|
+
|
|
106
|
+
**Semantic diff gate** — Patches that would modify too many instructions relative to the guard type are automatically rejected. The engine will never corrupt a complex function to patch a simple crash.
|
|
107
|
+
|
|
108
|
+
**Caller-aware propagation** — After patching a function, CodeSuture uses `gc.get_referrers` to find every live reference to the original code object — closures, bound methods, partials — and updates them all. No in-memory copy of the broken function survives.
|
|
109
|
+
|
|
110
|
+
**Shadow execution mode** — `--shadow` monitors the return value of patched functions. If a sentinel default (`""`, `0`, `None`) leaks into downstream logic, CodeSuture logs a warning before it causes a second failure.
|
|
111
|
+
|
|
112
|
+
**Patch expiry** — Every persisted patch carries a configurable TTL. When a patch ages past its limit, CodeSuture logs a reminder that the underlying bug should be addressed in source code. Patches are scaffolding, not permanent fixes.
|
|
113
|
+
|
|
114
|
+
**Bytecode fingerprint registry** — Crash sites are hashed by their surrounding instruction window. When the same crash pattern appears again, CodeSuture applies the known guard directly without re-running analysis.
|
|
115
|
+
|
|
116
|
+
**Audit trail** — `codesuture audit` displays every active patch: function name, guard type, target variable, default value, age, and rollback instructions. `codesuture explain` gives a plain-language breakdown of exactly what each patch changed and whether the default value is safe for downstream consumers.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## What CodeSuture is not
|
|
121
|
+
|
|
122
|
+
**Not a logger.** It does not record exceptions and move on. It patches the function and retries.
|
|
123
|
+
|
|
124
|
+
**Not a fuzzer or static analyzer.** It operates at runtime on live bytecode, not on source.
|
|
125
|
+
|
|
126
|
+
**Not autonomous.** Patches should be reviewed via `codesuture audit` and `codesuture explain`. The goal is to keep your program running while you address the root cause — not to replace the fix.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Limitations
|
|
131
|
+
|
|
132
|
+
**Python 3.11+ only.** CodeSuture depends on bytecode structures introduced in CPython 3.11. Earlier versions are not supported.
|
|
133
|
+
|
|
134
|
+
**List and dict comprehensions are not patchable.** Comprehensions are implemented as anonymous nested code objects in CPython. CodeSuture detects crashes inside them and logs a warning, but does not attempt to patch them. Refactor the comprehension into a named function to enable patching.
|
|
135
|
+
|
|
136
|
+
**Semantic bugs are not patchable.** CodeSuture fixes structural crashes — null access, missing keys, type mismatches, bounds errors. It cannot fix logic errors where the code runs but produces wrong results.
|
|
137
|
+
|
|
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
|
+
|
|
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
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT. See [LICENSE](LICENSE) for details.
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
|
@@ -25,6 +46,22 @@ def validate_patch(original_code, patched_code):
|
|
|
25
46
|
|
|
26
47
|
def propagate_patch(original_func, patched_code) -> int:
|
|
27
48
|
import gc
|
|
49
|
+
import logging
|
|
50
|
+
if original_func is None:
|
|
51
|
+
return 0
|
|
52
|
+
if not hasattr(original_func, '__code__'):
|
|
53
|
+
return 0
|
|
54
|
+
name = getattr(original_func, '__qualname__', '') or \
|
|
55
|
+
getattr(original_func, '__name__', '')
|
|
56
|
+
if '<listcomp>' in name or '<genexpr>' in name or \
|
|
57
|
+
'<dictcomp>' in name or '<setcomp>' in name:
|
|
58
|
+
import logging
|
|
59
|
+
logging.getLogger(__name__).debug(
|
|
60
|
+
"[CodeSuture] Skipping %s — "
|
|
61
|
+
"comprehensions are not patchable via __code__", name
|
|
62
|
+
)
|
|
63
|
+
return 0
|
|
64
|
+
|
|
28
65
|
original_code = original_func.__code__
|
|
29
66
|
propagated = 0
|
|
30
67
|
|
|
@@ -35,21 +72,91 @@ def propagate_patch(original_func, patched_code) -> int:
|
|
|
35
72
|
if hasattr(ref, '__func__') and hasattr(ref.__func__, '__code__'):
|
|
36
73
|
if ref.__func__.__code__ is original_code:
|
|
37
74
|
ref.__func__.__code__ = patched_code
|
|
75
|
+
_force_despecialize(ref.__func__)
|
|
38
76
|
propagated += 1
|
|
39
77
|
|
|
40
78
|
elif hasattr(ref, '__code__') and ref.__code__ is original_code:
|
|
41
79
|
ref.__code__ = patched_code
|
|
80
|
+
_force_despecialize(ref)
|
|
42
81
|
propagated += 1
|
|
43
82
|
|
|
44
83
|
original_func.__code__ = patched_code
|
|
84
|
+
_force_despecialize(original_func)
|
|
45
85
|
|
|
46
86
|
if propagated > 0:
|
|
47
87
|
print(f"[CodeSuture] Propagated patch to {propagated} additional "
|
|
48
88
|
f"live reference(s) of {original_func.__qualname__}.")
|
|
49
89
|
return propagated
|
|
50
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
|
+
|
|
51
148
|
def synthesize_guarded_code(original_code, spec: PatchSpec) -> Bytecode:
|
|
52
|
-
|
|
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'):
|
|
53
160
|
res = _build_subscript_guarded_code(original_code, spec.var_name, spec.key_name, spec.default_value)
|
|
54
161
|
elif spec.strategy == 'chain_subscript_guard':
|
|
55
162
|
res = _build_chain_subscript_guarded_code(original_code, spec.var_name, spec.key_name, spec.default_value)
|