scientific-method-engine 0.4.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.4.0 → scientific_method_engine-0.5.0}/PKG-INFO +10 -1
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/README.md +9 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/pyproject.toml +1 -1
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/cli.py +1 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/dispatch.py +1 -1
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/effect_order.py +9 -5
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/reports.py +5 -3
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/trace.py +97 -6
- scientific_method_engine-0.5.0/tests/test_table_continuations.py +161 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/.gitignore +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/LICENSE +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/__init__.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/__main__.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ClearNoReturnFunctions.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/CreateFunctions.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ExportBoundedFlow.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ExportFunctionFingerprints.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ExportFunctionInventory.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/MergeFallThroughFragment.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/RecoverCitedFunctions.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/RepairReturningCallers.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportCallArguments.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportCallPaths.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportCallSitesWithScalars.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportCallsToRange.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportConstantFirstArgumentCalls.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportDataBytes.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportDecompileMatches.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportDecompileWindow.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportFilePatternInMemory.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportFirstArgumentCallSummary.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportFunctionScalarConstants.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportFunctionSummary.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportInstructionContext.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportInstructionWindow.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportMemoryBlockForFileOffset.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportMemoryBlocks.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportRandomnessCandidates.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportReferences.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportScalarConstants.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportStringReferences.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/ghidra/ReportSymbolReferences.java +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/__init__.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/image.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/machine.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/pe.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/result_flow.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/src/scientific_method_engine/x86/values.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/tests/test_dispatch.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/tests/test_effect_order.py +0 -0
- {scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/tests/test_pe.py +0 -0
- {scientific_method_engine-0.4.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
|
|
@@ -113,3 +113,12 @@ The `effects` command additionally emits path-local `effectOrdering` timelines,
|
|
|
113
113
|
pre-call write prefixes and bounded local restoration witnesses. Unknown modeled
|
|
114
114
|
or nested service effects stay separate; no return code establishes rollback or
|
|
115
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).
|
|
@@ -101,3 +101,12 @@ The `effects` command additionally emits path-local `effectOrdering` timelines,
|
|
|
101
101
|
pre-call write prefixes and bounded local restoration witnesses. Unknown modeled
|
|
102
102
|
or nested service effects stay separate; no return code establishes rollback or
|
|
103
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"
|
|
@@ -10,6 +10,7 @@ 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
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"
|
|
13
14
|
" scientific-method-engine ghidra-scripts")
|
|
14
15
|
|
|
15
16
|
|
|
@@ -3,7 +3,7 @@ from bisect import bisect_right
|
|
|
3
3
|
|
|
4
4
|
|
|
5
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"}
|
|
6
|
+
"arithmetic", "value-transfer", "conversion", "flag-write", "flags-save", "flags-restore", "local-iret", "string-operation", "declared-jump-continuation"}
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _storage(event):
|
|
@@ -25,7 +25,10 @@ def effect_ordering(report):
|
|
|
25
25
|
aliases, external effects, resources or transactionality.
|
|
26
26
|
"""
|
|
27
27
|
summaries = []
|
|
28
|
-
|
|
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:
|
|
29
32
|
timeline, writes, calls, witnesses = [], [], [], []
|
|
30
33
|
snapshots = {}
|
|
31
34
|
pending = {}
|
|
@@ -78,13 +81,14 @@ def effect_ordering(report):
|
|
|
78
81
|
if path.get("stop"):
|
|
79
82
|
boundary = {"site": path.get("stopSite"), "reason": path["stop"],
|
|
80
83
|
"writesBeforeCount": len(writes), "meaning": "later effects are not read"}
|
|
81
|
-
|
|
84
|
+
destination = conditional_summaries if conditional else summaries
|
|
85
|
+
destination.append({"path": index, "declaredJumpAssumptions": path.get("declaredJumpAssumptions", []), "returned": path["returned"], "stop": boundary,
|
|
82
86
|
"guards": path["guards"], "timeline": timeline,
|
|
83
87
|
"writeOrders": [w["order"] for w in writes], "calls": calls,
|
|
84
88
|
"localRestorationWitnesses": witnesses,
|
|
85
|
-
"effectCompleteWithinModel": bool(path["returned"] and not unknown_orders),
|
|
89
|
+
"effectCompleteWithinModel": bool(path["returned"] and not unknown_orders and not conditional),
|
|
86
90
|
"transactionality": "not established; local writes and result codes cannot prove external rollback"})
|
|
87
|
-
report["effectOrdering"] = {"paths": summaries,
|
|
91
|
+
report["effectOrdering"] = {"paths": summaries, "declaredContinuationPaths": conditional_summaries,
|
|
88
92
|
"allPathsRead": report["completeWithinModel"],
|
|
89
93
|
"nativeReachability": "unconfirmed",
|
|
90
94
|
"meaning": "separate conditional paths; write prefixes index each path's writeOrders"}
|
|
@@ -226,7 +226,8 @@ def uses(image, config):
|
|
|
226
226
|
for root in established[index:]:
|
|
227
227
|
stops.setdefault(root, "entry not traced: entry or total instruction budget exhausted")
|
|
228
228
|
break
|
|
229
|
-
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)
|
|
230
231
|
remaining -= report["stepsUsed"]
|
|
231
232
|
string_remaining -= report["stringIterationsUsed"]
|
|
232
233
|
if not report["completeWithinModel"]:
|
|
@@ -496,7 +497,7 @@ def dispatch(image, config):
|
|
|
496
497
|
raise ValueError("Dispatch table mapping differs from PE source sections")
|
|
497
498
|
for value in values:
|
|
498
499
|
integer(value, 0, (1 << ALIASES[input_reg][2]) - 1, "input value")
|
|
499
|
-
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)
|
|
500
501
|
outcomes = []
|
|
501
502
|
for path in report["paths"]:
|
|
502
503
|
reached = bool(path["instructionPath"]) and path["instructionPath"][-1] == site
|
|
@@ -594,7 +595,8 @@ def allocations(report, config):
|
|
|
594
595
|
"observedExtentBytes": capacity,
|
|
595
596
|
"capacity": "conditional on evidenced extent units and pointer identity" if extent else "unresolved: request units and bounded writes do not establish allocated extent",
|
|
596
597
|
"rollback": "unproven; failure returns do not undo earlier writes"})
|
|
597
|
-
return {"allocations": results, "paths": report["paths"], "
|
|
598
|
+
return {"allocations": results, "paths": report["paths"], "declaredContinuationPaths": report["declaredContinuationPaths"],
|
|
599
|
+
"gaps": report["gaps"], "completeWithinModel": report["completeWithinModel"]}
|
|
598
600
|
|
|
599
601
|
|
|
600
602
|
def operand_provenance(image, config):
|
|
@@ -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,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
|
{scientific_method_engine-0.4.0 → scientific_method_engine-0.5.0}/tests/test_effect_order.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|