codesuture 0.5.0__py3-none-any.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.
- codesuture/__init__.py +1 -0
- codesuture/__main__.py +5 -0
- codesuture/_eval_fix.py +5 -0
- codesuture/audit.py +127 -0
- codesuture/cli.py +127 -0
- codesuture/code_replacer.py +82 -0
- codesuture/codesuture_fix.py +85 -0
- codesuture/debuggee.py +7 -0
- codesuture/diff_guard.py +27 -0
- codesuture/explain.py +147 -0
- codesuture/fingerprint.py +52 -0
- codesuture/guard_synthesizer.py +607 -0
- codesuture/knowledge.py +35 -0
- codesuture/middleware.py +86 -0
- codesuture/pattern_matcher.py +555 -0
- codesuture/persistence.py +330 -0
- codesuture/plugins/__init__.py +0 -0
- codesuture/plugins/autonomous.py +64 -0
- codesuture/rewind.py +43 -0
- codesuture/rollback.py +85 -0
- codesuture/sandbox.py +105 -0
- codesuture/shadow.py +20 -0
- codesuture/tracer.py +447 -0
- codesuture/watcher.py +78 -0
- codesuture-0.5.0.dist-info/METADATA +106 -0
- codesuture-0.5.0.dist-info/RECORD +30 -0
- codesuture-0.5.0.dist-info/WHEEL +5 -0
- codesuture-0.5.0.dist-info/entry_points.txt +2 -0
- codesuture-0.5.0.dist-info/licenses/LICENSE +33 -0
- codesuture-0.5.0.dist-info/top_level.txt +1 -0
codesuture/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
codesuture/__main__.py
ADDED
codesuture/_eval_fix.py
ADDED
codesuture/audit.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
def run_audit(patch_store_path: str = None):
|
|
7
|
+
|
|
8
|
+
candidates = [
|
|
9
|
+
".codesuture_cache", ".codesuture_store",
|
|
10
|
+
".codesuture", "codesuture_patches"
|
|
11
|
+
]
|
|
12
|
+
store = patch_store_path
|
|
13
|
+
if not store:
|
|
14
|
+
for c in candidates:
|
|
15
|
+
if os.path.exists(c):
|
|
16
|
+
store = c
|
|
17
|
+
break
|
|
18
|
+
|
|
19
|
+
if not store:
|
|
20
|
+
print("[CodeSuture] No patch store found. Nothing has been patched yet.")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
patches = _load_all_patches(store)
|
|
24
|
+
|
|
25
|
+
if not patches:
|
|
26
|
+
print("[CodeSuture] Patch store exists but is empty.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
now = datetime.utcnow()
|
|
30
|
+
|
|
31
|
+
col_func = max(18, max(len(p.get("func_name","?")) for p in patches) + 2)
|
|
32
|
+
col_guard = 12
|
|
33
|
+
col_target = 10
|
|
34
|
+
col_default = 10
|
|
35
|
+
col_age = 7
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
"|".encode(sys.stdout.encoding or 'ascii')
|
|
39
|
+
HAS_UNICODE = True
|
|
40
|
+
except Exception:
|
|
41
|
+
HAS_UNICODE = False
|
|
42
|
+
|
|
43
|
+
def row(f, g, t, d, a):
|
|
44
|
+
v = "|" if HAS_UNICODE else "|"
|
|
45
|
+
return (f"{v} {f:<{col_func}} {v} {g:<{col_guard}} {v} "
|
|
46
|
+
f"{t:<{col_target}} {v} {d:<{col_default}} {v} {a:<{col_age}} {v}")
|
|
47
|
+
|
|
48
|
+
if HAS_UNICODE:
|
|
49
|
+
sep = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
50
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+")
|
|
51
|
+
top = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
52
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+++")
|
|
53
|
+
bot = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
54
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+")
|
|
55
|
+
else:
|
|
56
|
+
sep = (f"|-{'*'*col_func}-+-{'*'*col_guard}-+-"
|
|
57
|
+
f"{'*'*col_target}-+-{'*'*col_default}-+-{'*'*col_age}-|")
|
|
58
|
+
top = sep
|
|
59
|
+
bot = sep
|
|
60
|
+
|
|
61
|
+
print()
|
|
62
|
+
print(" CodeSuture Audit Report")
|
|
63
|
+
print()
|
|
64
|
+
print(top)
|
|
65
|
+
print(row("Function", "Guard", "Target", "Default", "Age"))
|
|
66
|
+
print(sep)
|
|
67
|
+
|
|
68
|
+
oldest_days = 0
|
|
69
|
+
expired = 0
|
|
70
|
+
for p in patches:
|
|
71
|
+
func = p.get("func_name", "?")
|
|
72
|
+
guard = p.get("guard_type", "?")
|
|
73
|
+
target = p.get("target", "?")
|
|
74
|
+
default = repr(p.get("default_value", "?"))[:col_default]
|
|
75
|
+
age_str = "?"
|
|
76
|
+
ttl_days = p.get("ttl_days", 7)
|
|
77
|
+
if "patched_at" in p:
|
|
78
|
+
try:
|
|
79
|
+
dt = datetime.fromisoformat(p["patched_at"])
|
|
80
|
+
days = (now - dt).days
|
|
81
|
+
oldest_days = max(oldest_days, days)
|
|
82
|
+
age_str = f"{days}d"
|
|
83
|
+
if days > ttl_days:
|
|
84
|
+
age_str += " [WARN]"
|
|
85
|
+
expired += 1
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
print(row(func, guard, target, default, age_str))
|
|
89
|
+
|
|
90
|
+
print(bot)
|
|
91
|
+
print()
|
|
92
|
+
print(f" Total: {len(patches)} active patch(es). "
|
|
93
|
+
f"Oldest: {oldest_days}d. "
|
|
94
|
+
f"{'[WARN] ' + str(expired) + ' expired - run codesuture rollback to clear.' if expired else 'All within TTL.'}")
|
|
95
|
+
print()
|
|
96
|
+
print(" Run 'codesuture rollback <function_name>' to remove a patch.")
|
|
97
|
+
print(" Run 'codesuture rollback --all' to clear everything.")
|
|
98
|
+
print()
|
|
99
|
+
|
|
100
|
+
def _load_all_patches(store_path: str) -> list[dict]:
|
|
101
|
+
|
|
102
|
+
patches = []
|
|
103
|
+
if os.path.isdir(store_path):
|
|
104
|
+
for root, dirs, files in os.walk(store_path):
|
|
105
|
+
for fname in files:
|
|
106
|
+
fpath = os.path.join(root, fname)
|
|
107
|
+
if fname.endswith(".json") and os.path.isfile(fpath):
|
|
108
|
+
try:
|
|
109
|
+
with open(fpath, "r", encoding="utf-8") as f:
|
|
110
|
+
data = json.load(f)
|
|
111
|
+
if isinstance(data, list):
|
|
112
|
+
patches.extend(data)
|
|
113
|
+
elif isinstance(data, dict):
|
|
114
|
+
patches.append(data)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
elif os.path.isfile(store_path):
|
|
118
|
+
try:
|
|
119
|
+
with open(store_path, "r", encoding="utf-8") as f:
|
|
120
|
+
data = json.load(f)
|
|
121
|
+
if isinstance(data, list):
|
|
122
|
+
patches = data
|
|
123
|
+
elif isinstance(data, dict):
|
|
124
|
+
patches = list(data.values())
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
return patches
|
codesuture/cli.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
from codesuture.tracer import install, uninstall
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(prog='codesuture',
|
|
7
|
+
description='Runtime Python bytecode patcher with self-healing re-execution')
|
|
8
|
+
parser.add_argument('--version', action='version', version='codesuture 0.5.0')
|
|
9
|
+
sub = parser.add_subparsers(dest='command', required=True)
|
|
10
|
+
|
|
11
|
+
run_parser = sub.add_parser('run', help='Run a script with live patching')
|
|
12
|
+
run_parser.add_argument('script', help='Target script to run')
|
|
13
|
+
run_parser.add_argument('--dry-run', action='store_true', help='Show what would be patched without applying')
|
|
14
|
+
run_parser.add_argument('--log', metavar='FILE', help='Append patch events (JSON lines) to FILE')
|
|
15
|
+
run_parser.add_argument('--retries', type=int, default=3, metavar='N', help='Max patching attempts (default: 3)')
|
|
16
|
+
run_parser.add_argument('--self-test', action='store_true', help='Corrupt the engine to test self-healing')
|
|
17
|
+
run_parser.add_argument('--autonomous', action='store_true', help='Enable autonomous LLM bug-fixing')
|
|
18
|
+
run_parser.add_argument('--verbose', action='store_true', help='Show detailed debug output')
|
|
19
|
+
run_parser.add_argument('--shadow', action='store_true', help='Warn if patched functions return sentinel values')
|
|
20
|
+
run_parser.add_argument('--ttl', type=int, default=7, metavar='DAYS', help='Patch TTL in days (default: 7)')
|
|
21
|
+
|
|
22
|
+
sub.add_parser('audit', help='Show all active patches')
|
|
23
|
+
|
|
24
|
+
rb_parser = sub.add_parser('rollback', help='Remove persisted patches')
|
|
25
|
+
rb_parser.add_argument('function_name', nargs='?', default=None, help='Function name to roll back')
|
|
26
|
+
rb_parser.add_argument('--all', action='store_true', dest='rollback_all', help='Remove ALL patches + fingerprints')
|
|
27
|
+
rb_parser.add_argument('--dry-run', action='store_true', dest='rollback_dry_run', help='List what would be removed')
|
|
28
|
+
|
|
29
|
+
watch_parser = sub.add_parser('watch', help='Watch and auto-restart a script with live patching')
|
|
30
|
+
watch_parser.add_argument('script', help='Target script to watch')
|
|
31
|
+
watch_parser.add_argument('--max-restarts', type=int, default=10, metavar='N',
|
|
32
|
+
help='Maximum number of restarts (default: 10)')
|
|
33
|
+
watch_parser.add_argument('--shadow', action='store_true', help='Enable shadow mode warnings')
|
|
34
|
+
watch_parser.add_argument('--verbose', action='store_true', help='Show detailed debug output')
|
|
35
|
+
|
|
36
|
+
explain_parser = sub.add_parser('explain', help='Show detailed explanation of active patches')
|
|
37
|
+
explain_parser.add_argument('func_name', nargs='?', default=None, help='Function name to explain')
|
|
38
|
+
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
if args.command == 'audit':
|
|
42
|
+
from codesuture.audit import run_audit
|
|
43
|
+
run_audit()
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if args.command == 'rollback':
|
|
47
|
+
from codesuture.rollback import rollback_function, rollback_all, rollback_dry_run
|
|
48
|
+
if args.rollback_dry_run:
|
|
49
|
+
rollback_dry_run()
|
|
50
|
+
elif args.rollback_all:
|
|
51
|
+
rollback_all()
|
|
52
|
+
elif args.function_name:
|
|
53
|
+
rollback_function(args.function_name)
|
|
54
|
+
else:
|
|
55
|
+
print("[CodeSuture] Usage: codesuture rollback <function_name> | --all | --dry-run")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if args.command == 'watch':
|
|
59
|
+
from codesuture.watcher import watch
|
|
60
|
+
exit_code = watch(
|
|
61
|
+
args.script,
|
|
62
|
+
max_restarts=args.max_restarts,
|
|
63
|
+
shadow=args.shadow,
|
|
64
|
+
verbose=args.verbose,
|
|
65
|
+
)
|
|
66
|
+
sys.exit(exit_code)
|
|
67
|
+
|
|
68
|
+
if args.command == 'explain':
|
|
69
|
+
from codesuture.explain import run_explain
|
|
70
|
+
run_explain(args.func_name)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if getattr(args, 'autonomous', False):
|
|
74
|
+
try:
|
|
75
|
+
import llama_cpp
|
|
76
|
+
except ImportError:
|
|
77
|
+
print("Autonomous mode requires llama-cpp-python. Install with: pip install codesuture[autonomous]")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
if args.command == 'run':
|
|
81
|
+
from codesuture.persistence import install_import_hook, make_persisted_patch_globals
|
|
82
|
+
|
|
83
|
+
install_import_hook()
|
|
84
|
+
|
|
85
|
+
if getattr(args, 'self_test', False):
|
|
86
|
+
import codesuture.pattern_matcher as pm
|
|
87
|
+
print("[CodeSuture] SELF-TEST: corrupting _infer_default -> None")
|
|
88
|
+
pm._infer_default = None
|
|
89
|
+
tracer = None
|
|
90
|
+
try:
|
|
91
|
+
with open(args.script, 'r', encoding='utf-8') as f:
|
|
92
|
+
source = f.read()
|
|
93
|
+
code = compile(source, args.script, 'exec')
|
|
94
|
+
|
|
95
|
+
tracer = install(dry_run=args.dry_run, log_file=args.log,
|
|
96
|
+
max_retries=args.retries,
|
|
97
|
+
autonomous=getattr(args, 'autonomous', False),
|
|
98
|
+
script_path=args.script, verbose=args.verbose,
|
|
99
|
+
shadow=args.shadow, ttl=args.ttl)
|
|
100
|
+
max_runs = args.retries + 1
|
|
101
|
+
for run in range(max_runs):
|
|
102
|
+
patched_before = tracer.stats['patched']
|
|
103
|
+
tracer._handled_exc_ids.clear()
|
|
104
|
+
try:
|
|
105
|
+
sys.settrace(tracer)
|
|
106
|
+
globs = make_persisted_patch_globals(
|
|
107
|
+
"__main__",
|
|
108
|
+
{'__name__': '__main__', '__file__': args.script},
|
|
109
|
+
)
|
|
110
|
+
exec(code, globs)
|
|
111
|
+
break
|
|
112
|
+
except Exception as e:
|
|
113
|
+
sys.settrace(None)
|
|
114
|
+
new_patches = tracer.stats['patched'] - patched_before
|
|
115
|
+
if new_patches > 0 and run < max_runs - 1:
|
|
116
|
+
print(f"[CodeSuture] Re-executing after {new_patches} patch(es)...")
|
|
117
|
+
continue
|
|
118
|
+
else:
|
|
119
|
+
print(f"[CodeSuture] Script exited with: {e}")
|
|
120
|
+
break
|
|
121
|
+
finally:
|
|
122
|
+
uninstall()
|
|
123
|
+
if tracer is not None:
|
|
124
|
+
tracer.report()
|
|
125
|
+
|
|
126
|
+
if __name__ == '__main__':
|
|
127
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import types
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
def replace_function_code(func, new_code):
|
|
5
|
+
|
|
6
|
+
from codesuture.guard_synthesizer import propagate_patch
|
|
7
|
+
propagate_patch(func, new_code)
|
|
8
|
+
|
|
9
|
+
def get_function_from_frame(frame):
|
|
10
|
+
name = frame.f_code.co_name
|
|
11
|
+
code = frame.f_code
|
|
12
|
+
|
|
13
|
+
if name in frame.f_locals:
|
|
14
|
+
candidate = frame.f_locals[name]
|
|
15
|
+
if hasattr(candidate, '__code__') and candidate.__code__ is code:
|
|
16
|
+
return candidate
|
|
17
|
+
|
|
18
|
+
if name in frame.f_globals:
|
|
19
|
+
candidate = frame.f_globals[name]
|
|
20
|
+
if hasattr(candidate, '__code__') and candidate.__code__ is code:
|
|
21
|
+
return candidate
|
|
22
|
+
|
|
23
|
+
if name == '<module>':
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
self_obj = frame.f_locals.get('self')
|
|
27
|
+
if self_obj is not None:
|
|
28
|
+
cls = type(self_obj)
|
|
29
|
+
method = getattr(cls, name, None)
|
|
30
|
+
if method is not None:
|
|
31
|
+
if isinstance(method, property) and method.fget is not None:
|
|
32
|
+
func = method.fget
|
|
33
|
+
else:
|
|
34
|
+
func = method
|
|
35
|
+
if hasattr(func, '__func__'):
|
|
36
|
+
func = func.__func__
|
|
37
|
+
if hasattr(func, '__code__') and func.__code__ is code:
|
|
38
|
+
return func
|
|
39
|
+
|
|
40
|
+
cls_obj = frame.f_locals.get('cls')
|
|
41
|
+
if cls_obj is not None and isinstance(cls_obj, type):
|
|
42
|
+
method = getattr(cls_obj, name, None)
|
|
43
|
+
if method is not None:
|
|
44
|
+
if isinstance(method, property) and method.fget is not None:
|
|
45
|
+
func = method.fget
|
|
46
|
+
else:
|
|
47
|
+
func = method
|
|
48
|
+
if hasattr(func, '__func__'):
|
|
49
|
+
func = func.__func__
|
|
50
|
+
if hasattr(func, '__code__') and func.__code__ is code:
|
|
51
|
+
return func
|
|
52
|
+
|
|
53
|
+
for val in frame.f_globals.values():
|
|
54
|
+
if isinstance(val, type):
|
|
55
|
+
method = getattr(val, name, None)
|
|
56
|
+
if method is not None:
|
|
57
|
+
if isinstance(method, property) and method.fget is not None:
|
|
58
|
+
func = method.fget
|
|
59
|
+
else:
|
|
60
|
+
func = method
|
|
61
|
+
if hasattr(func, '__func__'):
|
|
62
|
+
func = func.__func__
|
|
63
|
+
if hasattr(func, '__code__') and func.__code__ is code:
|
|
64
|
+
return func
|
|
65
|
+
|
|
66
|
+
for val in frame.f_globals.values():
|
|
67
|
+
if hasattr(val, '__wrapped__'):
|
|
68
|
+
inner = val.__wrapped__
|
|
69
|
+
if hasattr(inner, '__code__') and inner.__code__ is code:
|
|
70
|
+
return inner
|
|
71
|
+
if hasattr(val, '__code__') and val.__code__ is code:
|
|
72
|
+
return val
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def get_source_from_frame(frame):
|
|
77
|
+
|
|
78
|
+
func = get_function_from_frame(frame)
|
|
79
|
+
try:
|
|
80
|
+
return inspect.getsource(func)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return f"# Could not get source: {e}\n"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
import types
|
|
5
|
+
from codesuture.pattern_matcher import analyze_exception
|
|
6
|
+
from codesuture.guard_synthesizer import synthesize_guarded_code
|
|
7
|
+
from codesuture.code_replacer import replace_function_code, get_function_from_frame
|
|
8
|
+
from codesuture.rewind import rewind_frame_to_start
|
|
9
|
+
|
|
10
|
+
def apply_fix(exc_type_name: str = None, exc_msg: str = None) -> str:
|
|
11
|
+
|
|
12
|
+
exc_info = sys.exc_info()
|
|
13
|
+
target_frame = None
|
|
14
|
+
tb = None
|
|
15
|
+
|
|
16
|
+
if exc_info[0] is not None:
|
|
17
|
+
if exc_type_name is None:
|
|
18
|
+
exc_type_name = exc_info[0].__name__
|
|
19
|
+
if exc_msg is None:
|
|
20
|
+
exc_msg = str(exc_info[1])
|
|
21
|
+
|
|
22
|
+
tb = exc_info[2]
|
|
23
|
+
curr_tb = tb
|
|
24
|
+
while curr_tb and curr_tb.tb_next:
|
|
25
|
+
curr_tb = curr_tb.tb_next
|
|
26
|
+
if curr_tb:
|
|
27
|
+
target_frame = curr_tb.tb_frame
|
|
28
|
+
|
|
29
|
+
if target_frame is None:
|
|
30
|
+
if exc_type_name is None or exc_msg is None:
|
|
31
|
+
return "ERROR: No active exception. You must provide exc_type_name and exc_msg if paused on an unhandled exception."
|
|
32
|
+
|
|
33
|
+
for fi in inspect.stack():
|
|
34
|
+
frame = fi.frame
|
|
35
|
+
filename = frame.f_code.co_filename
|
|
36
|
+
func_name = frame.f_code.co_name
|
|
37
|
+
|
|
38
|
+
if 'pydevd' in filename.lower() and func_name in ('evaluate_expression', 'new_func', '_run_with_unblock_threads', '_run_with_interrupt_thread'):
|
|
39
|
+
potential_frame = frame.f_locals.get('frame')
|
|
40
|
+
if isinstance(potential_frame, types.FrameType):
|
|
41
|
+
target_frame = potential_frame
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
if func_name in ('apply_fix', 'apply_fix_with_info', '<module>', 'Exec', 'exec'):
|
|
45
|
+
continue
|
|
46
|
+
if any(x in filename.lower() for x in ('pydevd', 'debugpy', 'threading', 'importlib')):
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
basename = os.path.basename(filename)
|
|
50
|
+
internal_files = (
|
|
51
|
+
'codesuture_fix.py', 'pattern_matcher.py', 'guard_synthesizer.py',
|
|
52
|
+
'code_replacer.py', 'rewind.py', 'tracer.py', 'debuggee.py'
|
|
53
|
+
)
|
|
54
|
+
if basename in internal_files:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
target_frame = frame
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
if target_frame is None:
|
|
61
|
+
return "ERROR: No paused frame found"
|
|
62
|
+
|
|
63
|
+
class FakeExc:
|
|
64
|
+
def __init__(self, msg):
|
|
65
|
+
self._msg = msg
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return self._msg
|
|
68
|
+
|
|
69
|
+
exc_type = FakeExc
|
|
70
|
+
exc_type.__name__ = exc_type_name
|
|
71
|
+
exc_value = FakeExc(exc_msg)
|
|
72
|
+
|
|
73
|
+
spec = analyze_exception(target_frame, exc_type, exc_value, tb)
|
|
74
|
+
if spec is None:
|
|
75
|
+
return f"ERROR: No deterministic patch for {exc_type_name}"
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
func = get_function_from_frame(target_frame)
|
|
79
|
+
new_bc = synthesize_guarded_code(target_frame.f_code, spec)
|
|
80
|
+
new_code = new_bc.to_code()
|
|
81
|
+
replace_function_code(func, new_code)
|
|
82
|
+
rewind_frame_to_start(target_frame, target_frame.f_code)
|
|
83
|
+
return f"OK: patched {target_frame.f_code.co_name} ({spec.strategy} on {spec.var_name})"
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return f"ERROR: {e}"
|
codesuture/debuggee.py
ADDED
codesuture/diff_guard.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import dis
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class DiffResult:
|
|
6
|
+
guard_type: str
|
|
7
|
+
added: int
|
|
8
|
+
removed: int
|
|
9
|
+
changed: int
|
|
10
|
+
allowed: int
|
|
11
|
+
rejected: bool
|
|
12
|
+
reason: str = ""
|
|
13
|
+
|
|
14
|
+
def semantic_diff(original_code, patched_code, guard_type: str) -> DiffResult:
|
|
15
|
+
orig_ops = [(i.opname, i.argval) for i in dis.get_instructions(original_code)]
|
|
16
|
+
patch_ops = [(i.opname, i.argval) for i in dis.get_instructions(patched_code)]
|
|
17
|
+
added = len([x for x in patch_ops if x not in orig_ops])
|
|
18
|
+
removed = len([x for x in orig_ops if x not in patch_ops])
|
|
19
|
+
changed = max(added, removed)
|
|
20
|
+
allowed = max(50, int(len(orig_ops) * 0.40))
|
|
21
|
+
rejected = changed > allowed
|
|
22
|
+
reason = (
|
|
23
|
+
f"Semantic diff too large for {guard_type}: "
|
|
24
|
+
f"{changed} instructions changed, allowed <= {allowed}. "
|
|
25
|
+
f"Patch was NOT applied."
|
|
26
|
+
) if rejected else ""
|
|
27
|
+
return DiffResult(guard_type, added, removed, changed, allowed, rejected, reason)
|
codesuture/explain.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
def run_explain(func_name=None):
|
|
8
|
+
|
|
9
|
+
candidates = [
|
|
10
|
+
".codesuture_cache", ".codesuture_store",
|
|
11
|
+
".codesuture", "codesuture_patches",
|
|
12
|
+
]
|
|
13
|
+
store = None
|
|
14
|
+
for c in candidates:
|
|
15
|
+
if os.path.exists(c):
|
|
16
|
+
store = c
|
|
17
|
+
break
|
|
18
|
+
|
|
19
|
+
if not store:
|
|
20
|
+
print("[CodeSuture] No active patches.")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
patches = _load_all_patches(store)
|
|
24
|
+
|
|
25
|
+
if not patches:
|
|
26
|
+
print("[CodeSuture] No active patches.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
if func_name:
|
|
30
|
+
patches = [p for p in patches if func_name.lower() in p.get("func_name", "").lower()]
|
|
31
|
+
if not patches:
|
|
32
|
+
print(f"[CodeSuture] No patches found for '{func_name}'.")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
now = datetime.utcnow()
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
"|".encode(sys.stdout.encoding or "ascii")
|
|
39
|
+
HAS_UNICODE = True
|
|
40
|
+
except Exception:
|
|
41
|
+
HAS_UNICODE = False
|
|
42
|
+
|
|
43
|
+
col_func = max(12, max(len(p.get("func_name", "?")) for p in patches) + 2)
|
|
44
|
+
col_guard = max(12, max(len(p.get("guard_type", "?")) for p in patches) + 2)
|
|
45
|
+
col_target = max(10, max(len(str(p.get("target", "?"))) for p in patches) + 2)
|
|
46
|
+
col_default = max(10, max(len(repr(p.get("default_value", "?"))[:15]) for p in patches) + 2)
|
|
47
|
+
col_age = 12
|
|
48
|
+
col_safe = 9
|
|
49
|
+
|
|
50
|
+
v = "|" if HAS_UNICODE else "|"
|
|
51
|
+
|
|
52
|
+
def row(f, g, t, d, a, s):
|
|
53
|
+
return (f"{v} {f:<{col_func}} {v} {g:<{col_guard}} {v} "
|
|
54
|
+
f"{t:<{col_target}} {v} {d:<{col_default}} {v} "
|
|
55
|
+
f"{a:<{col_age}} {v} {s:<{col_safe}} {v}")
|
|
56
|
+
|
|
57
|
+
if HAS_UNICODE:
|
|
58
|
+
sep = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
59
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+-{'-'*col_safe}-+")
|
|
60
|
+
top = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
61
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+-{'-'*col_safe}-+")
|
|
62
|
+
bot = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
63
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+-{'-'*col_safe}-+")
|
|
64
|
+
else:
|
|
65
|
+
sep = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
66
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+-{'-'*col_safe}-+")
|
|
67
|
+
top = sep
|
|
68
|
+
bot = sep
|
|
69
|
+
|
|
70
|
+
print()
|
|
71
|
+
print(" CodeSuture Explain - Active Patches")
|
|
72
|
+
print()
|
|
73
|
+
print(top)
|
|
74
|
+
print(row("Function", "Guard type", "Target", "Default value", "Age (days)", "Safe?"))
|
|
75
|
+
print(sep)
|
|
76
|
+
|
|
77
|
+
for p in patches:
|
|
78
|
+
func = p.get("func_name", "?")
|
|
79
|
+
guard = p.get("guard_type", "?")
|
|
80
|
+
target = str(p.get("target", "?"))
|
|
81
|
+
default = repr(p.get("default_value", "?"))[:15]
|
|
82
|
+
age_str = "?"
|
|
83
|
+
if "patched_at" in p:
|
|
84
|
+
try:
|
|
85
|
+
dt = datetime.fromisoformat(p["patched_at"])
|
|
86
|
+
days = (now - dt).days
|
|
87
|
+
age_str = f"{days}d ago (patched)"
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
safe = _assess_safety(p)
|
|
91
|
+
print(row(func, guard, target, default, age_str, safe))
|
|
92
|
+
|
|
93
|
+
print(bot)
|
|
94
|
+
print()
|
|
95
|
+
print(f" Total: {len(patches)} active patch(es).")
|
|
96
|
+
print()
|
|
97
|
+
|
|
98
|
+
def _assess_safety(patch_data):
|
|
99
|
+
|
|
100
|
+
default = patch_data.get("default_value")
|
|
101
|
+
guard_type = patch_data.get("guard_type", "")
|
|
102
|
+
target = str(patch_data.get("target", ""))
|
|
103
|
+
|
|
104
|
+
string_methods = {"strip", "upper", "lower", "title", "capitalize",
|
|
105
|
+
"casefold", "swapcase", "encode", "replace", "split"}
|
|
106
|
+
|
|
107
|
+
is_string_downstream = any(m in target.lower() for m in string_methods)
|
|
108
|
+
|
|
109
|
+
if default == "" and (guard_type == "null_guard" or is_string_downstream):
|
|
110
|
+
return "LIKELY"
|
|
111
|
+
|
|
112
|
+
if default == 0 and is_string_downstream:
|
|
113
|
+
return "RISKY"
|
|
114
|
+
|
|
115
|
+
if default == "" and guard_type in ("null_guard", "subscript_guard", "key_guard"):
|
|
116
|
+
return "LIKELY"
|
|
117
|
+
|
|
118
|
+
return "UNKNOWN"
|
|
119
|
+
|
|
120
|
+
def _load_all_patches(store_path):
|
|
121
|
+
|
|
122
|
+
patches = []
|
|
123
|
+
if os.path.isdir(store_path):
|
|
124
|
+
for root, dirs, files in os.walk(store_path):
|
|
125
|
+
for fname in files:
|
|
126
|
+
fpath = os.path.join(root, fname)
|
|
127
|
+
if fname.endswith(".json") and os.path.isfile(fpath):
|
|
128
|
+
try:
|
|
129
|
+
with open(fpath, "r", encoding="utf-8") as f:
|
|
130
|
+
data = json.load(f)
|
|
131
|
+
if isinstance(data, list):
|
|
132
|
+
patches.extend(data)
|
|
133
|
+
elif isinstance(data, dict):
|
|
134
|
+
patches.append(data)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
elif os.path.isfile(store_path):
|
|
138
|
+
try:
|
|
139
|
+
with open(store_path, "r", encoding="utf-8") as f:
|
|
140
|
+
data = json.load(f)
|
|
141
|
+
if isinstance(data, list):
|
|
142
|
+
patches = data
|
|
143
|
+
elif isinstance(data, dict):
|
|
144
|
+
patches = list(data.values())
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
return patches
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import dis
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
FINGERPRINT_FILE = ".codesuture_fingerprints"
|
|
8
|
+
|
|
9
|
+
def compute_fingerprint(code_obj, crash_offset: int, exc_type_name: str) -> str:
|
|
10
|
+
instructions = list(dis.get_instructions(code_obj))
|
|
11
|
+
offsets = [i.offset for i in instructions]
|
|
12
|
+
try:
|
|
13
|
+
idx = offsets.index(crash_offset)
|
|
14
|
+
except ValueError:
|
|
15
|
+
idx = len(instructions) - 1
|
|
16
|
+
window = instructions[max(0, idx-2):idx+3]
|
|
17
|
+
key = str([(i.opname, str(i.argval)) for i in window]) + ":" + exc_type_name
|
|
18
|
+
return hashlib.sha256(key.encode()).hexdigest()[:16]
|
|
19
|
+
|
|
20
|
+
def load_registry() -> dict:
|
|
21
|
+
if not os.path.exists(FINGERPRINT_FILE):
|
|
22
|
+
return {}
|
|
23
|
+
with open(FINGERPRINT_FILE, "r", encoding="utf-8") as f:
|
|
24
|
+
return json.load(f)
|
|
25
|
+
|
|
26
|
+
def save_registry(registry: dict):
|
|
27
|
+
with open(FINGERPRINT_FILE, "w", encoding="utf-8") as f:
|
|
28
|
+
json.dump(registry, f, indent=2)
|
|
29
|
+
|
|
30
|
+
def lookup(fingerprint: str) -> dict | None:
|
|
31
|
+
return load_registry().get(fingerprint)
|
|
32
|
+
|
|
33
|
+
def record(fingerprint: str, guard_type: str, target: str,
|
|
34
|
+
func_name: str, error_type: str, default_value=None, key_name=None):
|
|
35
|
+
registry = load_registry()
|
|
36
|
+
if fingerprint not in registry:
|
|
37
|
+
registry[fingerprint] = {
|
|
38
|
+
"guard_type": guard_type,
|
|
39
|
+
"target": target,
|
|
40
|
+
"error_type": error_type,
|
|
41
|
+
"default_value": default_value,
|
|
42
|
+
"key_name": key_name,
|
|
43
|
+
"first_seen": datetime.utcnow().isoformat(),
|
|
44
|
+
"hit_count": 0,
|
|
45
|
+
"functions": []
|
|
46
|
+
}
|
|
47
|
+
entry = registry[fingerprint]
|
|
48
|
+
entry["hit_count"] += 1
|
|
49
|
+
if func_name not in entry["functions"]:
|
|
50
|
+
entry["functions"].append(func_name)
|
|
51
|
+
save_registry(registry)
|
|
52
|
+
return entry
|