scientific-method-engine 0.3.0__tar.gz → 0.5.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.
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/PKG-INFO +15 -1
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/README.md +14 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/pyproject.toml +1 -1
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/cli.py +2 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/dispatch.py +1 -1
- scientific_method_engine-0.5.0/src/scientific_method_engine/x86/effect_order.py +95 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/reports.py +8 -3
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/trace.py +97 -6
- scientific_method_engine-0.5.0/tests/test_effect_order.py +114 -0
- scientific_method_engine-0.5.0/tests/test_table_continuations.py +161 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/.gitignore +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/LICENSE +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/__init__.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/__main__.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ClearNoReturnFunctions.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/CreateFunctions.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ExportBoundedFlow.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ExportFunctionFingerprints.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ExportFunctionInventory.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/MergeFallThroughFragment.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/RecoverCitedFunctions.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/RepairReturningCallers.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportCallArguments.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportCallPaths.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportCallSitesWithScalars.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportCallsToRange.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportConstantFirstArgumentCalls.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportDataBytes.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportDecompileMatches.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportDecompileWindow.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportFilePatternInMemory.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportFirstArgumentCallSummary.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportFunctionScalarConstants.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportFunctionSummary.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportInstructionContext.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportInstructionWindow.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportMemoryBlockForFileOffset.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportMemoryBlocks.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportRandomnessCandidates.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportReferences.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportScalarConstants.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportStringReferences.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportSymbolReferences.java +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/__init__.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/image.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/machine.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/pe.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/result_flow.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/values.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/tests/test_dispatch.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/tests/test_pe.py +0 -0
- {scientific_method_engine-0.3.0 → scientific_method_engine-0.5.0}/tests/test_x86.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scientific-method-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Bounded instruction-derived x86 evidence reports for segmented MZ/FBOV and PE32/i386 code.
|
|
5
5
|
Project-URL: Source, https://github.com/kibertoad/refurbished-dinosaurs-toolkit/tree/main/packages/scientific-method-engine
|
|
6
6
|
Author: kibertoad
|
|
@@ -108,3 +108,17 @@ Commands, inputs, limits and acceptance rules are in
|
|
|
108
108
|
[the bounded evidence reporter guide](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md).
|
|
109
109
|
The reader and engine check that they speak the same prepared-config protocol and refuse to run
|
|
110
110
|
otherwise.
|
|
111
|
+
|
|
112
|
+
The `effects` command additionally emits path-local `effectOrdering` timelines,
|
|
113
|
+
pre-call write prefixes and bounded local restoration witnesses. Unknown modeled
|
|
114
|
+
or nested service effects stay separate; no return code establishes rollback or
|
|
115
|
+
transactionality. See [the full contract](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md#ordered-effect-path-summaries).
|
|
116
|
+
|
|
117
|
+
Evidenced segmented16 `indirectJumps` declarations also expose separate
|
|
118
|
+
`declaredContinuationPaths`. Ordinary paths retain their unresolved transfer;
|
|
119
|
+
conditional routes preserve prefix/child effects with target-choice, live-table
|
|
120
|
+
and selector assumptions. Effects summaries never mark those routes complete.
|
|
121
|
+
Concrete values/field addresses reject inconsistent rows; overlap and shared
|
|
122
|
+
path/step/visit/total/boundary limits remain explicit. Partial-table splits are
|
|
123
|
+
partial evidence, never complete dispatch or native-reachability claims. See the
|
|
124
|
+
[table contract](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md#evidenced-indirect-jump-tables).
|
|
@@ -96,3 +96,17 @@ Commands, inputs, limits and acceptance rules are in
|
|
|
96
96
|
[the bounded evidence reporter guide](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md).
|
|
97
97
|
The reader and engine check that they speak the same prepared-config protocol and refuse to run
|
|
98
98
|
otherwise.
|
|
99
|
+
|
|
100
|
+
The `effects` command additionally emits path-local `effectOrdering` timelines,
|
|
101
|
+
pre-call write prefixes and bounded local restoration witnesses. Unknown modeled
|
|
102
|
+
or nested service effects stay separate; no return code establishes rollback or
|
|
103
|
+
transactionality. See [the full contract](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md#ordered-effect-path-summaries).
|
|
104
|
+
|
|
105
|
+
Evidenced segmented16 `indirectJumps` declarations also expose separate
|
|
106
|
+
`declaredContinuationPaths`. Ordinary paths retain their unresolved transfer;
|
|
107
|
+
conditional routes preserve prefix/child effects with target-choice, live-table
|
|
108
|
+
and selector assumptions. Effects summaries never mark those routes complete.
|
|
109
|
+
Concrete values/field addresses reject inconsistent rows; overlap and shared
|
|
110
|
+
path/step/visit/total/boundary limits remain explicit. Partial-table splits are
|
|
111
|
+
partial evidence, never complete dispatch or native-reachability claims. See the
|
|
112
|
+
[table contract](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md#evidenced-indirect-jump-tables).
|
|
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scientific-method-engine"
|
|
7
7
|
# The release workflow writes the published version from the package's release tag.
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.5.0"
|
|
9
9
|
description = "Bounded instruction-derived x86 evidence reports for segmented MZ/FBOV and PE32/i386 code."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -9,6 +9,8 @@ CONFIG_LIMIT = 1024 * 1024
|
|
|
9
9
|
PREPARED_CONFIG_LIMIT = 16 * 1024 * 1024
|
|
10
10
|
USAGE = ("Usage: scientific-method-engine <operand|operand-candidates|target|bounds|owner|callees|trace|uses|arguments|"
|
|
11
11
|
"effects|returns|memory|incoming|call-order|guards|allocation|dispatch> <config.json|->\n"
|
|
12
|
+
"effects includes ordered path writes/calls and local restoration witnesses; transactionality remains unestablished.\n"
|
|
13
|
+
"Declared table continuations are separate conditional paths; ordinary computed transfers remain stopped.\n"
|
|
12
14
|
" scientific-method-engine ghidra-scripts")
|
|
13
15
|
|
|
14
16
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Ordered, path-local effect evidence; no transactional or native-execution claims."""
|
|
2
|
+
from bisect import bisect_right
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
KINDS = {"read", "write", "call", "call-return", "return", "branch", "compare", "flag-assumption",
|
|
6
|
+
"arithmetic", "value-transfer", "conversion", "flag-write", "flags-save", "flags-restore", "local-iret", "string-operation", "declared-jump-continuation"}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _storage(event):
|
|
10
|
+
# Equal offsets alone cannot identify storage. Retain the segment/base expression
|
|
11
|
+
# and the complete access width; a narrower restore is a separate access.
|
|
12
|
+
if not all(k in event for k in ("segment", "offset", "width")):
|
|
13
|
+
return None
|
|
14
|
+
# Terms are hashable tuples; compare them directly instead of serializing each access.
|
|
15
|
+
return (event["segment"].get("expression"), event["offset"].get("expression"), event["width"],
|
|
16
|
+
event.get("segmentInterpretation"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def effect_ordering(report):
|
|
20
|
+
"""Add bounded path timelines and explicit local restoration witnesses.
|
|
21
|
+
|
|
22
|
+
The existing tracer bounds every event. Prefix counts index a path's write
|
|
23
|
+
list instead of copying that list for every call (linear report size). A
|
|
24
|
+
witness restores one read value only; it says nothing about other storage,
|
|
25
|
+
aliases, external effects, resources or transactionality.
|
|
26
|
+
"""
|
|
27
|
+
summaries = []
|
|
28
|
+
conditional_summaries = []
|
|
29
|
+
combined = [(False, i, p) for i, p in enumerate(report["paths"])]
|
|
30
|
+
combined += [(True, i, p) for i, p in enumerate(report.get("declaredContinuationPaths", []))]
|
|
31
|
+
for conditional, index, path in combined:
|
|
32
|
+
timeline, writes, calls, witnesses = [], [], [], []
|
|
33
|
+
snapshots = {}
|
|
34
|
+
pending = {}
|
|
35
|
+
unknown_orders = []
|
|
36
|
+
last_write = {}
|
|
37
|
+
for event in path["events"]:
|
|
38
|
+
kind = event["kind"]
|
|
39
|
+
if kind in KINDS:
|
|
40
|
+
timeline.append(event)
|
|
41
|
+
if kind == "read":
|
|
42
|
+
key = _storage(event)
|
|
43
|
+
if key is not None:
|
|
44
|
+
snapshots.setdefault(key, {})[event["value"].get("expression")] = event
|
|
45
|
+
elif kind == "write":
|
|
46
|
+
key = _storage(event)
|
|
47
|
+
original = snapshots.get(key, {}).get(event["value"].get("expression"))
|
|
48
|
+
# An equal expression alone is not a restore: an independent constant store or a
|
|
49
|
+
# re-pushed return address can match it. The written value must derive from the read.
|
|
50
|
+
if (original is not None and last_write.get(key, -1) > original["order"]
|
|
51
|
+
and original["site"] in event["value"].get("producers", ())):
|
|
52
|
+
witnesses.append({"readOrder": original["order"], "restoreOrder": event["order"],
|
|
53
|
+
"entry": event["entry"], "width": event["width"],
|
|
54
|
+
"pathReturned": path["returned"],
|
|
55
|
+
"unknownEffectsBetweenCount": len(unknown_orders) - bisect_right(unknown_orders, original["order"]),
|
|
56
|
+
"meaning": "same complete storage/value expression restored locally; not transactionality"})
|
|
57
|
+
writes.append(event)
|
|
58
|
+
last_write[key] = event["order"]
|
|
59
|
+
elif kind == "call":
|
|
60
|
+
call = {"order": event["order"], "site": event["site"], "entry": event["entry"],
|
|
61
|
+
"depth": event["depth"], "target": event.get("target"),
|
|
62
|
+
"writesBeforeCount": len(writes), "status": "unresolved-or-stopped",
|
|
63
|
+
"unknownEffects": True, "continuation": None, "_unknownStart": len(unknown_orders)}
|
|
64
|
+
calls.append(call)
|
|
65
|
+
pending.setdefault((event["site"], event["depth"]), []).append(call)
|
|
66
|
+
elif kind == "call-return":
|
|
67
|
+
stack = pending.get((event["callSite"], event["depth"]), [])
|
|
68
|
+
if stack:
|
|
69
|
+
call = stack.pop()
|
|
70
|
+
modeled = event.get("modeled", False)
|
|
71
|
+
call.update(status="modeled-return" if modeled else "traced-return",
|
|
72
|
+
unknownEffects=bool(modeled or event.get("unknownMemoryEffects") or len(unknown_orders) > call.pop("_unknownStart")),
|
|
73
|
+
returnOrder=event["order"], writesAfterCount=len(writes),
|
|
74
|
+
continuation="assumes balanced returning service; its memory/flag effects are unknown" if modeled
|
|
75
|
+
else "local callee return reached within the instruction model")
|
|
76
|
+
if event.get("modeled") or event.get("unknownMemoryEffects"):
|
|
77
|
+
unknown_orders.append(event["order"])
|
|
78
|
+
for call in calls:
|
|
79
|
+
call.pop("_unknownStart", None)
|
|
80
|
+
boundary = None
|
|
81
|
+
if path.get("stop"):
|
|
82
|
+
boundary = {"site": path.get("stopSite"), "reason": path["stop"],
|
|
83
|
+
"writesBeforeCount": len(writes), "meaning": "later effects are not read"}
|
|
84
|
+
destination = conditional_summaries if conditional else summaries
|
|
85
|
+
destination.append({"path": index, "declaredJumpAssumptions": path.get("declaredJumpAssumptions", []), "returned": path["returned"], "stop": boundary,
|
|
86
|
+
"guards": path["guards"], "timeline": timeline,
|
|
87
|
+
"writeOrders": [w["order"] for w in writes], "calls": calls,
|
|
88
|
+
"localRestorationWitnesses": witnesses,
|
|
89
|
+
"effectCompleteWithinModel": bool(path["returned"] and not unknown_orders and not conditional),
|
|
90
|
+
"transactionality": "not established; local writes and result codes cannot prove external rollback"})
|
|
91
|
+
report["effectOrdering"] = {"paths": summaries, "declaredContinuationPaths": conditional_summaries,
|
|
92
|
+
"allPathsRead": report["completeWithinModel"],
|
|
93
|
+
"nativeReachability": "unconfirmed",
|
|
94
|
+
"meaning": "separate conditional paths; write prefixes index each path's writeOrders"}
|
|
95
|
+
return report
|
|
@@ -5,6 +5,7 @@ from capstone import CS_AC_READ, CS_AC_WRITE
|
|
|
5
5
|
from capstone.x86 import X86_OP_IMM, X86_OP_MEM, X86_OP_REG
|
|
6
6
|
from .machine import State, StopPath, REGISTERS, ALIASES, segment_register
|
|
7
7
|
from .values import unknown
|
|
8
|
+
from .effect_order import effect_ordering
|
|
8
9
|
from .result_flow import return_flows
|
|
9
10
|
from .image import Image, integer
|
|
10
11
|
from .trace import (trace, walk, call_target, unsupported_transfer, uncovered, base_mnemonic, OVERLAP_REASON, CONTESTED_REASON,
|
|
@@ -225,7 +226,8 @@ def uses(image, config):
|
|
|
225
226
|
for root in established[index:]:
|
|
226
227
|
stops.setdefault(root, "entry not traced: entry or total instruction budget exhausted")
|
|
227
228
|
break
|
|
228
|
-
report = trace(image, {**config, "entry": at, "totalSteps": remaining, "stringIterations": string_remaining}
|
|
229
|
+
report = trace(image, {**config, "entry": at, "totalSteps": remaining, "stringIterations": string_remaining},
|
|
230
|
+
continue_declared_jumps=False)
|
|
229
231
|
remaining -= report["stepsUsed"]
|
|
230
232
|
string_remaining -= report["stringIterationsUsed"]
|
|
231
233
|
if not report["completeWithinModel"]:
|
|
@@ -495,7 +497,7 @@ def dispatch(image, config):
|
|
|
495
497
|
raise ValueError("Dispatch table mapping differs from PE source sections")
|
|
496
498
|
for value in values:
|
|
497
499
|
integer(value, 0, (1 << ALIASES[input_reg][2]) - 1, "input value")
|
|
498
|
-
report = trace(image, {**config, "registers": {**config.get("registers", {}), input_reg: value}})
|
|
500
|
+
report = trace(image, {**config, "registers": {**config.get("registers", {}), input_reg: value}}, continue_declared_jumps=False)
|
|
499
501
|
outcomes = []
|
|
500
502
|
for path in report["paths"]:
|
|
501
503
|
reached = bool(path["instructionPath"]) and path["instructionPath"][-1] == site
|
|
@@ -593,7 +595,8 @@ def allocations(report, config):
|
|
|
593
595
|
"observedExtentBytes": capacity,
|
|
594
596
|
"capacity": "conditional on evidenced extent units and pointer identity" if extent else "unresolved: request units and bounded writes do not establish allocated extent",
|
|
595
597
|
"rollback": "unproven; failure returns do not undo earlier writes"})
|
|
596
|
-
return {"allocations": results, "paths": report["paths"], "
|
|
598
|
+
return {"allocations": results, "paths": report["paths"], "declaredContinuationPaths": report["declaredContinuationPaths"],
|
|
599
|
+
"gaps": report["gaps"], "completeWithinModel": report["completeWithinModel"]}
|
|
597
600
|
|
|
598
601
|
|
|
599
602
|
def operand_provenance(image, config):
|
|
@@ -1379,6 +1382,8 @@ def _run_report(image, config, command):
|
|
|
1379
1382
|
report = near_pointer_provenance(report, config)
|
|
1380
1383
|
if command == "allocation":
|
|
1381
1384
|
return allocations(report, config)
|
|
1385
|
+
if command == "effects":
|
|
1386
|
+
report = effect_ordering(report)
|
|
1382
1387
|
if command == "returns":
|
|
1383
1388
|
report = return_flows(report, config)
|
|
1384
1389
|
if command != "trace":
|
|
@@ -214,7 +214,14 @@ def snapshot(state):
|
|
|
214
214
|
return {name: state.reg(name).report() for name in ALIASES}
|
|
215
215
|
|
|
216
216
|
|
|
217
|
-
def trace(image, config):
|
|
217
|
+
def trace(image, config, continue_declared_jumps=True):
|
|
218
|
+
"""Trace bounded paths, preserving declared-table continuations as separate conditional evidence.
|
|
219
|
+
|
|
220
|
+
Ordinary paths run first. A path stopped at a declared indirect jump is then
|
|
221
|
+
continued once per surviving table target, so the continuations never take
|
|
222
|
+
budget from an ordinary path. Callers that read only ordinary paths pass
|
|
223
|
+
continue_declared_jumps=False.
|
|
224
|
+
"""
|
|
218
225
|
entry = integer(config.get("entry"), 0, len(image.data) - 1, "entry")
|
|
219
226
|
if not any(entry in r["entries"] for r in image.regions):
|
|
220
227
|
raise ValueError("Trace entry must be an established region entry")
|
|
@@ -246,6 +253,11 @@ def trace(image, config):
|
|
|
246
253
|
if r not in ALIASES or type(n) is not int or not 0 <= n < 1 << ALIASES[r][2]:
|
|
247
254
|
raise ValueError("Invalid model register")
|
|
248
255
|
pending, outputs, global_gaps = [State(entry, image, config)], [], []
|
|
256
|
+
conditional_outputs = []
|
|
257
|
+
# States stopped at a declared jump site, continued after the ordinary paths.
|
|
258
|
+
deferred = []
|
|
259
|
+
boundary_cache = {}
|
|
260
|
+
boundary_budget = integer(config.get("instructionLimit", 10000), 1, 100000, "instruction limit")
|
|
249
261
|
created = 1
|
|
250
262
|
total_steps = 0
|
|
251
263
|
total_string_steps = 0
|
|
@@ -264,11 +276,88 @@ def trace(image, config):
|
|
|
264
276
|
string_effect(s, ins, count, remaining)
|
|
265
277
|
|
|
266
278
|
def finish(s, reason=None, returned=False):
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
path = {"returned": returned, "stop": reason, "stopSite": None if returned else s.at, "steps": s.steps,
|
|
280
|
+
"instructionPath": s.path, "guards": s.guards, "events": s.events,
|
|
281
|
+
"registers": snapshot(s), "conditionalModels": s.conditional}
|
|
282
|
+
assumptions = getattr(s, "declared_jump_assumptions", [])
|
|
283
|
+
if assumptions:
|
|
284
|
+
path["declaredJumpAssumptions"] = assumptions
|
|
285
|
+
conditional_outputs.append(path)
|
|
286
|
+
else:
|
|
287
|
+
outputs.append(path)
|
|
270
288
|
|
|
271
|
-
|
|
289
|
+
def declared_continuations(s):
|
|
290
|
+
# Each child follows one declared target under a recorded assumption. The operand
|
|
291
|
+
# keeps its traced value; no register or table word is assigned. s is a copy taken
|
|
292
|
+
# at the jump, so the operand read below never enters the stopped ordinary path.
|
|
293
|
+
nonlocal created, boundary_budget
|
|
294
|
+
declaration = image.indirect_jumps[s.at]
|
|
295
|
+
ins = image.decode(s.at)
|
|
296
|
+
root_entry = s.frames[-1]["entry"]
|
|
297
|
+
if root_entry not in boundary_cache:
|
|
298
|
+
if boundary_budget < 1:
|
|
299
|
+
global_gaps.append({"site": s.at, "reason": "conditional table boundary instruction limit"})
|
|
300
|
+
return
|
|
301
|
+
seen, walk_gaps, _, undecoded, contested = walk(image, [root_entry], boundary_budget)
|
|
302
|
+
boundary_budget -= max(1, len(seen) + len(undecoded) + len(contested))
|
|
303
|
+
# A truncated walk never saw the instructions that could contest a target start.
|
|
304
|
+
if any(g["reason"] == "instruction limit" for g in walk_gaps):
|
|
305
|
+
seen = {}
|
|
306
|
+
boundary_cache[root_entry] = seen
|
|
307
|
+
seen = boundary_cache[root_entry]
|
|
308
|
+
try:
|
|
309
|
+
value = s.get(ins, ins.operands[0], image)
|
|
310
|
+
address = s.address(ins, ins.operands[0]) if ins.operands[0].type == X86_OP_MEM else None
|
|
311
|
+
except StopPath as error:
|
|
312
|
+
global_gaps.append({"site": s.at, "reason": "conditional table operand unresolved: " + str(error)})
|
|
313
|
+
return
|
|
314
|
+
groups = {}
|
|
315
|
+
region = image.region(s.at)
|
|
316
|
+
choice_key = repr(value.term)
|
|
317
|
+
previous = getattr(s, "declared_jump_choices", {}).get(choice_key)
|
|
318
|
+
for row in declaration["rows"]:
|
|
319
|
+
if address is not None:
|
|
320
|
+
segment, offset, _ = address
|
|
321
|
+
# A table word inside another declared region uses that region's mapping; one
|
|
322
|
+
# outside every region is read as an extension of the jump's region.
|
|
323
|
+
row_region = image.region(row["operandSite"]) or region
|
|
324
|
+
row_offset = row_region["ip"] + row["operandSite"] - row_region["start"]
|
|
325
|
+
if segment.number is not None and offset.number is not None:
|
|
326
|
+
if not 0 <= row_offset <= 65534 or segment.number * 16 + offset.number != row_region["segment"] * 16 + row_offset:
|
|
327
|
+
continue
|
|
328
|
+
if value.number is not None and value.number != row["rawOffset"]:
|
|
329
|
+
continue
|
|
330
|
+
if previous is not None and row["rawOffset"] not in previous:
|
|
331
|
+
continue
|
|
332
|
+
groups.setdefault(row["target"], []).append(row)
|
|
333
|
+
for target, rows in groups.items():
|
|
334
|
+
if target not in seen:
|
|
335
|
+
global_gaps.append({"site": s.at, "target": target,
|
|
336
|
+
"reason": "conditional table target boundary is unresolved or instruction-limited"})
|
|
337
|
+
continue
|
|
338
|
+
if created >= max_paths:
|
|
339
|
+
global_gaps.append({"site": s.at, "reason": "path limit"})
|
|
340
|
+
break
|
|
341
|
+
child = deepcopy(s)
|
|
342
|
+
child.declared_jump_choices = {**getattr(child, "declared_jump_choices", {}),
|
|
343
|
+
choice_key: {r["rawOffset"] for r in rows}}
|
|
344
|
+
assumption = {"site": s.at, "target": target, "tableIndices": [r["index"] for r in rows],
|
|
345
|
+
"operandSites": [r["operandSite"] for r in rows], "exhaustive": declaration["exhaustive"],
|
|
346
|
+
"evidence": declaration["evidence"], "tableEvidence": declaration["table"]["evidence"],
|
|
347
|
+
"operand": value.report(),
|
|
348
|
+
"operandAddress": None if address is None else
|
|
349
|
+
{"segment": address[0].report(), "offset": address[1].report(), "segmentRegister": address[2]},
|
|
350
|
+
"meaning": "conditional target choice from source table; selector, live table contents and reachability unverified"}
|
|
351
|
+
child.declared_jump_assumptions = [*getattr(child, "declared_jump_assumptions", []), assumption]
|
|
352
|
+
child.event("declared-jump-continuation", **{k: v for k, v in assumption.items() if k != "site"})
|
|
353
|
+
child.at = target
|
|
354
|
+
pending.append(child)
|
|
355
|
+
created += 1
|
|
356
|
+
|
|
357
|
+
while pending or deferred:
|
|
358
|
+
if not pending:
|
|
359
|
+
declared_continuations(deferred.pop(0))
|
|
360
|
+
continue
|
|
272
361
|
state = pending.pop()
|
|
273
362
|
try:
|
|
274
363
|
while True:
|
|
@@ -468,6 +557,8 @@ def trace(image, config):
|
|
|
468
557
|
if m in ("jmp", "ljmp"):
|
|
469
558
|
target, provenance = call_target(image, at, ins)
|
|
470
559
|
if target is None:
|
|
560
|
+
if continue_declared_jumps and at in image.indirect_jumps:
|
|
561
|
+
deferred.append(deepcopy(state))
|
|
471
562
|
raise StopPath("unresolved jump: " + provenance.get("reason", "outside mapped code"))
|
|
472
563
|
if m == "ljmp":
|
|
473
564
|
target_region = image.region(target)
|
|
@@ -519,7 +610,7 @@ def trace(image, config):
|
|
|
519
610
|
state.at = following
|
|
520
611
|
except StopPath as error:
|
|
521
612
|
finish(state, str(error))
|
|
522
|
-
return {"paths": outputs, "gaps": global_gaps,
|
|
613
|
+
return {"paths": outputs, "declaredContinuationPaths": conditional_outputs, "gaps": global_gaps,
|
|
523
614
|
"completeWithinModel": not global_gaps and bool(outputs) and all(p["returned"] for p in outputs),
|
|
524
615
|
"nativeReachability": "unconfirmed", "stepsUsed": total_steps, "stringIterationsUsed": total_string_steps,
|
|
525
616
|
"limits": {"steps": max_steps, "paths": max_paths, "depth": max_depth}}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Synthetic path evidence; no original bytes or claims."""
|
|
2
|
+
import unittest
|
|
3
|
+
from test_x86 import Code, report
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EffectOrderTests(unittest.TestCase):
|
|
7
|
+
def paths(self, result):
|
|
8
|
+
return result["effectOrdering"]["paths"]
|
|
9
|
+
|
|
10
|
+
def test_early_exit_has_distinct_write_contract_at_common_return(self):
|
|
11
|
+
c = Code().emit("85 c0").branch("74", "return").emit("c7 06 20 00 01 00").label("return").emit("c3")
|
|
12
|
+
r = report(c, "effects")
|
|
13
|
+
self.assertTrue(r["effectOrdering"]["allPathsRead"])
|
|
14
|
+
self.assertEqual(sorted(len(p["writeOrders"]) for p in self.paths(r)), [0, 1])
|
|
15
|
+
self.assertTrue(all(p["guards"] for p in self.paths(r)))
|
|
16
|
+
self.assertTrue(all(p["transactionality"].startswith("not established") for p in self.paths(r)))
|
|
17
|
+
|
|
18
|
+
def test_writes_before_modeled_failure_and_later_predicate_remain_separate(self):
|
|
19
|
+
c = Code().emit("c7 06 20 00 01 00").label("service").branch("e8", "serviceBody")
|
|
20
|
+
c.emit("83 3e 22 00 00").branch("74", "return").emit("c7 06 24 00 02 00").label("return").emit("c3")
|
|
21
|
+
c.label("serviceBody").emit("c3")
|
|
22
|
+
r = report(c, "effects", callModels=[{"site": c.labels["service"], "evidence": "synthetic conditional failure",
|
|
23
|
+
"cases": [{"registers": {"ax": 1}}]}])
|
|
24
|
+
for p in self.paths(r):
|
|
25
|
+
call = p["calls"][0]
|
|
26
|
+
self.assertEqual(call["writesBeforeCount"], 1)
|
|
27
|
+
self.assertEqual(call["status"], "modeled-return")
|
|
28
|
+
self.assertTrue(call["unknownEffects"])
|
|
29
|
+
self.assertFalse(p["effectCompleteWithinModel"])
|
|
30
|
+
branch = next(e for e in p["timeline"] if e["kind"] == "branch")
|
|
31
|
+
compare = next(e for e in p["timeline"] if e["kind"] == "compare")
|
|
32
|
+
self.assertGreater(compare["order"], call["returnOrder"])
|
|
33
|
+
self.assertGreater(branch["order"], compare["order"])
|
|
34
|
+
self.assertEqual(branch["flagProducer"], compare["site"])
|
|
35
|
+
|
|
36
|
+
def test_child_writes_are_not_lost_in_caller_bracket(self):
|
|
37
|
+
c = Code().emit("c7 06 20 00 01 00").branch("e8", "child").emit("c7 06 20 00 00 00 c3")
|
|
38
|
+
c.label("child").emit("c7 06 20 00 03 00 c3")
|
|
39
|
+
p = self.paths(report(c, "effects", registers={"ds": 0x2000, "ss": 0x3000, "sp": 0xff00}))[0]
|
|
40
|
+
writes = [e for e in p["timeline"] if e["kind"] == "write" and e.get("role") != "stack"]
|
|
41
|
+
self.assertTrue(any(e["entry"] == c.labels["child"] and e["value"]["value"] == 3 for e in writes))
|
|
42
|
+
self.assertEqual(p["calls"][0]["status"], "traced-return")
|
|
43
|
+
self.assertFalse(p["calls"][0]["unknownEffects"])
|
|
44
|
+
|
|
45
|
+
def test_unknown_nested_service_propagates_to_outer_call(self):
|
|
46
|
+
c = Code().branch("e8", "child").emit("c3").label("child").label("service").branch("e8", "external").emit("c3")
|
|
47
|
+
c.label("external").emit("c3")
|
|
48
|
+
r = report(c, "effects", callModels=[{"site": c.labels["service"], "evidence": "unknown child service", "cases": [{}]}])
|
|
49
|
+
p = self.paths(r)[0]
|
|
50
|
+
self.assertTrue(all(call["unknownEffects"] for call in p["calls"]))
|
|
51
|
+
self.assertFalse(p["effectCompleteWithinModel"])
|
|
52
|
+
|
|
53
|
+
def test_snapshot_restore_witness_does_not_cover_bypass_or_other_storage(self):
|
|
54
|
+
c = Code().emit("a1 20 00 c7 06 20 00 02 00 85 db").branch("74", "return")
|
|
55
|
+
c.emit("a3 20 00").label("return").emit("c3")
|
|
56
|
+
r = report(c, "effects")
|
|
57
|
+
self.assertEqual(sorted(len(p["localRestorationWitnesses"]) for p in self.paths(r)), [0, 1])
|
|
58
|
+
w = next(w for p in self.paths(r) for w in p["localRestorationWitnesses"])
|
|
59
|
+
self.assertEqual(w["width"], 2)
|
|
60
|
+
self.assertTrue(w["pathReturned"])
|
|
61
|
+
self.assertEqual(w["unknownEffectsBetweenCount"], 0)
|
|
62
|
+
for code in ["a1 20 00 c7 06 20 00 02 00 a3 22 00 c3", # other storage
|
|
63
|
+
"a1 20 00 c7 06 20 00 02 00 a2 20 00 c3", # partial width
|
|
64
|
+
"a1 20 00 c7 06 20 00 02 00 b8 03 00 a3 20 00 c3", # changed value
|
|
65
|
+
"a1 20 00 c7 06 20 00 02 00 26 a3 20 00 c3"]: # other segment
|
|
66
|
+
self.assertFalse(self.paths(report(code, "effects"))[0]["localRestorationWitnesses"])
|
|
67
|
+
|
|
68
|
+
def test_equal_value_without_read_provenance_is_not_a_restore(self):
|
|
69
|
+
# An independent immediate store equals the value read earlier.
|
|
70
|
+
p = self.paths(report("c7 06 20 00 00 00 83 3e 20 00 00 c7 06 20 00 05 00 c7 06 20 00 00 00 c3", "effects"))[0]
|
|
71
|
+
self.assertFalse(p["localRestorationWitnesses"])
|
|
72
|
+
# A loop re-pushes the same return address into a stack slot another call overwrote.
|
|
73
|
+
c = Code().emit("b9 02 00").label("top").branch("e8", "a").branch("e8", "b").emit("49").branch("75", "top").emit("c3")
|
|
74
|
+
c.label("a").emit("c3").label("b").emit("c3")
|
|
75
|
+
r = report(c, "effects", registers={"ss": 0x3000, "sp": 0xff00})
|
|
76
|
+
self.assertTrue(any(p["returned"] for p in self.paths(r)))
|
|
77
|
+
self.assertFalse([w for p in self.paths(r) for w in p["localRestorationWitnesses"]])
|
|
78
|
+
|
|
79
|
+
def test_timeline_keeps_flag_assumptions_that_split_paths(self):
|
|
80
|
+
r = report("b9 02 00 f3 a4 c3", "effects")
|
|
81
|
+
self.assertEqual(len(self.paths(r)), 2)
|
|
82
|
+
values = sorted(next(e for e in p["timeline"] if e["kind"] == "flag-assumption")["value"]["value"] for p in self.paths(r))
|
|
83
|
+
self.assertEqual(values, [0, 1])
|
|
84
|
+
|
|
85
|
+
def test_restore_prefix_is_not_a_completed_return(self):
|
|
86
|
+
p = self.paths(report("a1 20 00 c7 06 20 00 02 00 a3 20 00 ee", "effects"))[0]
|
|
87
|
+
self.assertTrue(p["localRestorationWitnesses"])
|
|
88
|
+
self.assertFalse(p["localRestorationWitnesses"][0]["pathReturned"])
|
|
89
|
+
self.assertFalse(p["effectCompleteWithinModel"])
|
|
90
|
+
|
|
91
|
+
def test_unknown_service_between_snapshot_and_restore_remains_visible(self):
|
|
92
|
+
c = Code().emit("a1 20 00 50 c7 06 20 00 02 00").label("service").branch("e8", "child")
|
|
93
|
+
c.emit("58 a3 20 00 c3").label("child").emit("c3")
|
|
94
|
+
r = report(c, "effects", callModels=[{"site": c.labels["service"], "evidence": "unknown external effects", "cases": [{}]}])
|
|
95
|
+
p = self.paths(r)[0]
|
|
96
|
+
self.assertFalse(p["effectCompleteWithinModel"])
|
|
97
|
+
# The model invalidates stack memory too; no surviving snapshot value is guessed.
|
|
98
|
+
self.assertFalse(p["localRestorationWitnesses"])
|
|
99
|
+
|
|
100
|
+
def test_port_stop_and_path_caps_cannot_prove_absent_later_effects(self):
|
|
101
|
+
p = self.paths(report("c7 06 20 00 01 00 ee c7 06 22 00 02 00 c3", "effects"))[0]
|
|
102
|
+
self.assertFalse(p["returned"])
|
|
103
|
+
self.assertEqual(p["stop"]["writesBeforeCount"], 1)
|
|
104
|
+
self.assertIn("port", p["stop"]["reason"].lower())
|
|
105
|
+
r = report("85 c0 74 06 c7 06 20 00 01 00 c3", "effects", maxPaths=1)
|
|
106
|
+
self.assertFalse(r["effectOrdering"]["allPathsRead"])
|
|
107
|
+
self.assertTrue(r["gaps"])
|
|
108
|
+
r = report("c7 06 20 00 01 00 c3", "effects", maxSteps=1)
|
|
109
|
+
self.assertFalse(self.paths(r)[0]["returned"])
|
|
110
|
+
self.assertEqual(self.paths(r)[0]["stop"]["writesBeforeCount"], 1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
unittest.main()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Conditional source-table routes never replace unresolved execution evidence."""
|
|
2
|
+
import copy
|
|
3
|
+
import unittest
|
|
4
|
+
from test_dispatch import fixture
|
|
5
|
+
from scientific_method_engine.x86.reports import run_report
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TableContinuationTests(unittest.TestCase):
|
|
9
|
+
def test_stopped_path_is_retained_beside_conditional_child_routes(self):
|
|
10
|
+
data, config = fixture()
|
|
11
|
+
r = run_report(data, config, "effects")
|
|
12
|
+
self.assertFalse(r["completeWithinModel"])
|
|
13
|
+
self.assertFalse(any(p["returned"] for p in r["paths"]))
|
|
14
|
+
routes = r["declaredContinuationPaths"]
|
|
15
|
+
self.assertEqual(len(routes), 2)
|
|
16
|
+
self.assertTrue(all(p["returned"] for p in routes))
|
|
17
|
+
self.assertTrue(all(p["declaredJumpAssumptions"] for p in routes))
|
|
18
|
+
self.assertTrue(any(e["kind"] == "call" for p in routes for e in p["events"]))
|
|
19
|
+
self.assertTrue(all(not p["effectCompleteWithinModel"] for p in r["effectOrdering"]["declaredContinuationPaths"]))
|
|
20
|
+
|
|
21
|
+
def test_partial_table_and_concrete_mismatch_remain_unresolved(self):
|
|
22
|
+
data, config = fixture()
|
|
23
|
+
config["indirectJumps"][0]["exhaustive"] = False
|
|
24
|
+
r = run_report(data, config, "trace")
|
|
25
|
+
self.assertTrue(r["declaredContinuationPaths"])
|
|
26
|
+
self.assertTrue(all(not p["declaredJumpAssumptions"][0]["exhaustive"] for p in r["declaredContinuationPaths"]))
|
|
27
|
+
r = run_report(data, {**config, "registers": {"bx": 20}}, "trace")
|
|
28
|
+
self.assertFalse(r["declaredContinuationPaths"])
|
|
29
|
+
self.assertFalse(r["completeWithinModel"])
|
|
30
|
+
|
|
31
|
+
def test_duplicate_targets_share_one_conditional_route(self):
|
|
32
|
+
data, config = fixture()
|
|
33
|
+
data = bytearray(data)
|
|
34
|
+
data[34:36] = data[32:34]
|
|
35
|
+
r = run_report(bytes(data), config, "trace")
|
|
36
|
+
self.assertEqual(len(r["declaredContinuationPaths"]), 1)
|
|
37
|
+
self.assertEqual(r["declaredContinuationPaths"][0]["declaredJumpAssumptions"][0]["tableIndices"], [0, 1])
|
|
38
|
+
|
|
39
|
+
def test_overlap_targets_cannot_establish_a_continuation(self):
|
|
40
|
+
data, config = fixture()
|
|
41
|
+
data = bytearray(data)
|
|
42
|
+
data[8:12] = bytes.fromhex("b8 e8 00 c3")
|
|
43
|
+
data[34:36] = bytes.fromhex("09 00")
|
|
44
|
+
r = run_report(bytes(data), config, "trace")
|
|
45
|
+
self.assertFalse(r["declaredContinuationPaths"])
|
|
46
|
+
self.assertTrue(any("boundary" in g["reason"] for g in r["gaps"]))
|
|
47
|
+
|
|
48
|
+
def test_limits_do_not_leave_a_positive_continuation_contract(self):
|
|
49
|
+
data, config = fixture()
|
|
50
|
+
for change in ({"maxPaths": 1}, {"maxSteps": 1}, {"totalSteps": 1}, {"instructionLimit": 1}):
|
|
51
|
+
r = run_report(data, {**config, **change}, "trace")
|
|
52
|
+
self.assertFalse(any(p["returned"] for p in r["declaredContinuationPaths"]))
|
|
53
|
+
self.assertFalse(r["completeWithinModel"])
|
|
54
|
+
|
|
55
|
+
def test_conditional_loop_keeps_visits_and_prefix_write(self):
|
|
56
|
+
data, config = fixture()
|
|
57
|
+
data = bytearray(data)
|
|
58
|
+
data[0:8] = bytes.fromhex("c7 06 20 00 01 00 ff e3")
|
|
59
|
+
data[16:18] = bytes.fromhex("eb fe")
|
|
60
|
+
config = copy.deepcopy(config)
|
|
61
|
+
config["indirectJumps"][0]["site"] = 6
|
|
62
|
+
r = run_report(bytes(data), {**config, "visitLimit": 1}, "effects")
|
|
63
|
+
self.assertTrue(any(p["returned"] for p in r["declaredContinuationPaths"]))
|
|
64
|
+
self.assertTrue(any(p["stop"] and "repeated" in p["stop"] for p in r["declaredContinuationPaths"]))
|
|
65
|
+
self.assertTrue(all(p["timeline"][0]["kind"] == "write" for p in r["effectOrdering"]["declaredContinuationPaths"]))
|
|
66
|
+
|
|
67
|
+
def test_repeated_operand_cannot_choose_a_contradictory_target(self):
|
|
68
|
+
data, config = fixture()
|
|
69
|
+
data = bytearray(data)
|
|
70
|
+
data[16:18] = bytes.fromhex("eb ee")
|
|
71
|
+
r = run_report(bytes(data), {**config, "visitLimit": 3}, "trace")
|
|
72
|
+
returned = [p for p in r["declaredContinuationPaths"] if p["returned"]]
|
|
73
|
+
self.assertTrue(returned)
|
|
74
|
+
self.assertTrue(all({a["target"] for a in p["declaredJumpAssumptions"]} == {8} for p in returned))
|
|
75
|
+
self.assertTrue(any(p["stop"] and "repeated" in p["stop"] for p in r["declaredContinuationPaths"]))
|
|
76
|
+
|
|
77
|
+
def test_resolved_memory_field_selects_only_its_declared_row(self):
|
|
78
|
+
data, config = fixture()
|
|
79
|
+
data = bytearray(data)
|
|
80
|
+
data[0:3] = bytes.fromhex("2e ff 27")
|
|
81
|
+
r = run_report(bytes(data), {**config, "registers": {"bx": 32}}, "trace")
|
|
82
|
+
self.assertEqual(len(r["declaredContinuationPaths"]), 1)
|
|
83
|
+
a = r["declaredContinuationPaths"][0]["declaredJumpAssumptions"][0]
|
|
84
|
+
self.assertEqual(a["tableIndices"], [0])
|
|
85
|
+
self.assertEqual(a["operandAddress"]["segmentRegister"], "cs")
|
|
86
|
+
r = run_report(bytes(data), {**config, "registers": {"bx": 36}}, "trace")
|
|
87
|
+
self.assertFalse(r["declaredContinuationPaths"])
|
|
88
|
+
|
|
89
|
+
def test_prefix_mutation_precedes_child_failure_and_bypasses_clear(self):
|
|
90
|
+
data = bytearray([0x90] * 80)
|
|
91
|
+
data[0:8] = bytes.fromhex("c7 06 20 00 01 00 ff e3")
|
|
92
|
+
data[16:27] = bytes.fromhex("e8 1d 00 85 c0 74 05 b8 ff ff c3")
|
|
93
|
+
data[28:35] = bytes.fromhex("c7 06 20 00 00 00 c3")
|
|
94
|
+
data[40:44] = bytes.fromhex("b8 00 00 c3")
|
|
95
|
+
data[48:52] = bytes.fromhex("b8 ff ff c3")
|
|
96
|
+
data[64:68] = bytes.fromhex("10 00 28 00")
|
|
97
|
+
config = {"entry": 0, "registers": {"ds": 0x2000, "ss": 0x3000, "sp": 0xff00},
|
|
98
|
+
"regions": [{"name": "synthetic", "start": 0, "end": 56, "ip": 0, "segment": 0x1000,
|
|
99
|
+
"entries": [0], "evidence": "constructed bounded code"}],
|
|
100
|
+
"indirectJumps": [{"site": 6, "exhaustive": True, "evidence": "synthetic BX choice",
|
|
101
|
+
"table": {"start": 64, "count": 2, "stride": 2, "evidence": "synthetic word targets"}}]}
|
|
102
|
+
r = run_report(bytes(data), config, "effects")
|
|
103
|
+
failure = next(p for p in r["declaredContinuationPaths"] if p["returned"] and p["registers"]["ax"]["value"] == 65535)
|
|
104
|
+
write = next(e for e in failure["events"] if e["kind"] == "write" and e["site"] == 0)
|
|
105
|
+
call = next(e for e in failure["events"] if e["kind"] == "call" and e["site"] == 16)
|
|
106
|
+
self.assertLess(write["order"], call["order"])
|
|
107
|
+
self.assertFalse(any(e["kind"] == "write" and e["site"] == 28 for e in failure["events"]))
|
|
108
|
+
self.assertFalse(r["completeWithinModel"])
|
|
109
|
+
self.assertTrue(all(not p["effectCompleteWithinModel"] for p in r["effectOrdering"]["declaredContinuationPaths"]))
|
|
110
|
+
|
|
111
|
+
def test_continuations_never_take_budget_from_ordinary_paths(self):
|
|
112
|
+
data = bytearray([0x90] * 52)
|
|
113
|
+
data[0:2] = bytes.fromhex("72 1e")
|
|
114
|
+
data[2:8] = bytes.fromhex("85 c0 74 01 c3 c3")
|
|
115
|
+
data[32:34] = bytes.fromhex("ff e3")
|
|
116
|
+
data[40:45] = bytes.fromhex("85 c9 74 00 c3")
|
|
117
|
+
data[48:52] = bytes.fromhex("28 00 2c 00")
|
|
118
|
+
config = {"entry": 0, "maxPaths": 3,
|
|
119
|
+
"regions": [{"name": "code", "start": 0, "end": 48, "segment": 4096, "ip": 0,
|
|
120
|
+
"entries": [0], "evidence": "constructed mappings"}],
|
|
121
|
+
"indirectJumps": [{"site": 32, "evidence": "constructed BX consumer", "exhaustive": True,
|
|
122
|
+
"table": {"start": 48, "count": 2, "stride": 2, "evidence": "constructed words"}}]}
|
|
123
|
+
declared = run_report(bytes(data), config, "trace")
|
|
124
|
+
plain = run_report(bytes(data), {k: v for k, v in config.items() if k != "indirectJumps"}, "trace")
|
|
125
|
+
self.assertEqual([(p["returned"], p["stop"]) for p in declared["paths"]],
|
|
126
|
+
[(p["returned"], p["stop"]) for p in plain["paths"]])
|
|
127
|
+
self.assertEqual(len(declared["paths"]), 3)
|
|
128
|
+
self.assertTrue(any(g["reason"] == "path limit" and g["site"] == 32 for g in declared["gaps"]))
|
|
129
|
+
|
|
130
|
+
def test_operand_read_stays_out_of_the_stopped_ordinary_path(self):
|
|
131
|
+
data, config = fixture()
|
|
132
|
+
data = bytearray(data)
|
|
133
|
+
data[0:3] = bytes.fromhex("2e ff 27")
|
|
134
|
+
r = run_report(bytes(data), {**config, "registers": {"bx": 32}}, "trace")
|
|
135
|
+
self.assertFalse(any(e["kind"] == "read" for e in r["paths"][0]["events"]))
|
|
136
|
+
self.assertTrue(any(e["kind"] == "read" for e in r["declaredContinuationPaths"][0]["events"]))
|
|
137
|
+
|
|
138
|
+
def test_truncated_boundary_walk_establishes_no_target(self):
|
|
139
|
+
data, config = fixture()
|
|
140
|
+
r = run_report(data, {**config, "instructionLimit": 2}, "trace")
|
|
141
|
+
self.assertFalse(r["declaredContinuationPaths"])
|
|
142
|
+
self.assertTrue(any("boundary" in g["reason"] for g in r["gaps"]))
|
|
143
|
+
|
|
144
|
+
def test_field_address_uses_the_table_region_mapping(self):
|
|
145
|
+
data, config = fixture()
|
|
146
|
+
data = bytearray(data)
|
|
147
|
+
data[0:3] = bytes.fromhex("2e ff 27")
|
|
148
|
+
config = copy.deepcopy(config)
|
|
149
|
+
config["regions"][0]["end"] = 32
|
|
150
|
+
config["regions"].append({"name": "table", "start": 32, "end": 40, "segment": 4096, "ip": 0x100,
|
|
151
|
+
"entries": [36], "evidence": "constructed table mapping"})
|
|
152
|
+
r = run_report(bytes(data), {**config, "registers": {"bx": 0x100}}, "trace")
|
|
153
|
+
self.assertEqual([p["declaredJumpAssumptions"][0]["tableIndices"] for p in r["declaredContinuationPaths"]], [[0]])
|
|
154
|
+
r = run_report(bytes(data), {**config, "registers": {"bx": 32}}, "trace")
|
|
155
|
+
self.assertFalse(r["declaredContinuationPaths"])
|
|
156
|
+
|
|
157
|
+
def test_allocation_retains_the_conditional_routes(self):
|
|
158
|
+
data, config = fixture()
|
|
159
|
+
allocation = run_report(data, {**config, "allocations": [{"site": 8, "unitBytes": 16, "unitEvidence": "constructed unit",
|
|
160
|
+
"requestRegister": "bx"}]}, "allocation")
|
|
161
|
+
self.assertEqual(len(allocation["declaredContinuationPaths"]), 2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|