unidecompiler-cli 0.2.2__tar.gz → 0.2.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unidecompiler-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Command-line host for unidecompiler plugins
5
5
  Author-email: Wker <1670133844@qq.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -11,7 +11,8 @@ Requires-Python: >=3.11
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: unidecompiler<0.3.0,>=0.2.2
13
13
  Requires-Dist: unidecompiler-export<0.3.0,>=0.2.2
14
- Requires-Dist: unidecompiler-simulator<0.3.0,>=0.2.2
14
+ Requires-Dist: unidecompiler-simulator<0.3.0,>=0.2.3
15
+ Requires-Dist: unidecompiler-symbolic<0.3.0,>=0.2.3
15
16
  Requires-Dist: unidecompiler-simulation-host-python<0.3.0,>=0.2.2
16
17
 
17
18
  # unidecompiler-cli
@@ -111,6 +112,23 @@ separate simulator library:
111
112
  unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
112
113
  ```
113
114
 
115
+ Bounded symbolic execution uses the same opaque frontend query and explores
116
+ the recovered generic IR only. `--symbolic` names scalar symbolic parameters;
117
+ all other parameters are supplied through `--concrete`:
118
+
119
+ ```sh
120
+ unidecompiler symbolic sample.pyc --function add \
121
+ --symbolic '{"left":{"sort":"int"}}' --concrete '{"right":5}'
122
+ ```
123
+
124
+ Use `--format text` for a human-readable summary. Exploration is bounded by
125
+ `--max-paths`, `--max-steps`, `--max-loop-unroll`, `--max-call-depth`, and
126
+ `--solver-timeout-ms`; symbolic sorts are `bool`, `int`, `real`, and `bitvec`
127
+ with a positive `bit_width`. JSON output contains each path's constraints,
128
+ model, return/raise value, and CFG trace. Non-success statuses such as
129
+ `unsupported`, `solver_timeout`, limits, `cancelled`, and `invalid_request`
130
+ are returned explicitly and produce a non-zero exit code.
131
+
114
132
  For trusted programs that require functions outside the lifted module, pass a
115
133
  Python environment file. Top-level functions are matched by name, while their
116
134
  stdout and stderr are returned as structured simulation events:
@@ -95,6 +95,23 @@ separate simulator library:
95
95
  unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
96
96
  ```
97
97
 
98
+ Bounded symbolic execution uses the same opaque frontend query and explores
99
+ the recovered generic IR only. `--symbolic` names scalar symbolic parameters;
100
+ all other parameters are supplied through `--concrete`:
101
+
102
+ ```sh
103
+ unidecompiler symbolic sample.pyc --function add \
104
+ --symbolic '{"left":{"sort":"int"}}' --concrete '{"right":5}'
105
+ ```
106
+
107
+ Use `--format text` for a human-readable summary. Exploration is bounded by
108
+ `--max-paths`, `--max-steps`, `--max-loop-unroll`, `--max-call-depth`, and
109
+ `--solver-timeout-ms`; symbolic sorts are `bool`, `int`, `real`, and `bitvec`
110
+ with a positive `bit_width`. JSON output contains each path's constraints,
111
+ model, return/raise value, and CFG trace. Non-success statuses such as
112
+ `unsupported`, `solver_timeout`, limits, `cancelled`, and `invalid_request`
113
+ are returned explicitly and produce a non-zero exit code.
114
+
98
115
  For trusted programs that require functions outside the lifted module, pass a
99
116
  Python environment file. Top-level functions are matched by name, while their
100
117
  stdout and stderr are returned as structured simulation events:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unidecompiler-cli"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Command-line host for unidecompiler plugins"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -13,7 +13,8 @@ requires-python = ">=3.11"
13
13
  dependencies = [
14
14
  "unidecompiler>=0.2.2,<0.3.0",
15
15
  "unidecompiler-export>=0.2.2,<0.3.0",
16
- "unidecompiler-simulator>=0.2.2,<0.3.0",
16
+ "unidecompiler-simulator>=0.2.3,<0.3.0",
17
+ "unidecompiler-symbolic>=0.2.3,<0.3.0",
17
18
  "unidecompiler-simulation-host-python>=0.2.2,<0.3.0",
18
19
  ]
19
20
 
@@ -24,11 +24,15 @@ from unidecompiler.progress import ProgressEvent, ProgressReporter
24
24
 
25
25
  def main(argv: list[str] | None = None, *, registry: FrontendRegistry | None = None) -> int:
26
26
  command_argv = sys.argv[1:] if argv is None else argv
27
- if command_argv and command_argv[0] in {"simulate", "template", "export-template"}:
27
+ if command_argv and command_argv[0] in {"simulate", "symbolic", "template", "export-template"}:
28
28
  if command_argv[0] in {"template", "export-template"}:
29
29
  from unidecompiler_cli.templates import main as template_main
30
30
 
31
31
  return template_main(command_argv[1:])
32
+ if command_argv[0] == "symbolic":
33
+ from unidecompiler_cli.symbolic import main as symbolic_main
34
+
35
+ return symbolic_main(command_argv[1:], registry=registry)
32
36
  from unidecompiler_cli.simulation import main as simulate_main
33
37
 
34
38
  return simulate_main(command_argv[1:], registry=registry)
@@ -40,6 +44,7 @@ def main(argv: list[str] | None = None, *, registry: FrontendRegistry | None = N
40
44
  " template export a VM frontend or GUI plugin starter project\n"
41
45
  " export-template alias for template\n"
42
46
  " simulate run a recovered generic-IR function\n\n"
47
+ " symbolic explore recovered generic-IR paths\n\n"
43
48
  "Template examples:\n"
44
49
  " unidecompiler template frontend MyVM -o ./my-vm --author NAME "
45
50
  "--description TEXT --requirements TEXT --suffix .vm --version 1\n"
@@ -0,0 +1,148 @@
1
+ """Symbolic execution command hosted by the unidecompiler CLI package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+ import sys
9
+ from typing import Any
10
+
11
+ import z3
12
+
13
+ from unidecompiler.plugin_registry import FrontendRegistry
14
+ from unidecompiler_symbolic import (
15
+ SymbolicEngine,
16
+ SymbolicInput,
17
+ SymbolicLimits,
18
+ SymbolicSort,
19
+ SymbolicStatus,
20
+ )
21
+
22
+
23
+ def main(argv: list[str] | None = None, *, registry: FrontendRegistry | None = None) -> int:
24
+ parser = argparse.ArgumentParser(prog="unidecompiler symbolic")
25
+ parser.add_argument("input")
26
+ parser.add_argument("--function", required=True, help="frontend-owned function query")
27
+ parser.add_argument("--frontend")
28
+ parser.add_argument("--symbolic", default="{}", help="JSON object of symbolic input specifications")
29
+ parser.add_argument("--concrete", default="{}", help="JSON object of concrete parameter values")
30
+ parser.add_argument("--max-paths", type=int, default=256)
31
+ parser.add_argument("--max-steps", type=int, default=100_000)
32
+ parser.add_argument("--max-loop-unroll", type=int, default=32)
33
+ parser.add_argument("--max-call-depth", type=int, default=32)
34
+ parser.add_argument("--solver-timeout-ms", type=int, default=5_000)
35
+ parser.add_argument("--format", choices=("json", "text"), default="json")
36
+ args = parser.parse_args(argv)
37
+ try:
38
+ symbolic_inputs = _symbolic_inputs(args.symbolic)
39
+ concrete_args = _object(args.concrete, "--concrete")
40
+ limits = SymbolicLimits(
41
+ args.max_paths, args.max_steps, args.max_loop_unroll,
42
+ args.max_call_depth, args.solver_timeout_ms,
43
+ )
44
+ limits.validate()
45
+ engine = SymbolicEngine.from_registry(registry or FrontendRegistry.discover())
46
+ result = engine.explore_path(
47
+ Path(args.input), args.function, frontend_id=args.frontend,
48
+ symbolic_inputs=symbolic_inputs, concrete_args=concrete_args, limits=limits,
49
+ )
50
+ except (OSError, ValueError, json.JSONDecodeError) as error:
51
+ print(f"invalid symbolic request: {error}", file=sys.stderr)
52
+ return 2
53
+ if args.format == "text":
54
+ print(_text_result(result))
55
+ else:
56
+ print(json.dumps(_result_payload(result), ensure_ascii=False, sort_keys=True, allow_nan=False))
57
+ return _exit_code(result.status)
58
+
59
+
60
+ def _object(value: str, option: str) -> dict[str, Any]:
61
+ parsed = json.loads(value)
62
+ if not isinstance(parsed, dict) or not all(isinstance(key, str) for key in parsed):
63
+ raise ValueError(f"{option} must be a JSON object with string keys")
64
+ return parsed
65
+
66
+
67
+ def _symbolic_inputs(value: str) -> tuple[SymbolicInput, ...]:
68
+ result: list[SymbolicInput] = []
69
+ for name, spec in _object(value, "--symbolic").items():
70
+ if not isinstance(spec, dict):
71
+ raise ValueError("each --symbolic input must be an object")
72
+ unknown = set(spec) - {"sort", "bit_width"}
73
+ if unknown:
74
+ raise ValueError(f"unknown symbolic input fields for {name!r}: {', '.join(sorted(unknown))}")
75
+ try:
76
+ sort = SymbolicSort(spec.get("sort", SymbolicSort.INT.value))
77
+ except ValueError as error:
78
+ raise ValueError(f"invalid symbolic sort for {name!r}") from error
79
+ width = spec.get("bit_width")
80
+ if width is not None and (not isinstance(width, int) or isinstance(width, bool)):
81
+ raise ValueError(f"bit_width for {name!r} must be an integer")
82
+ item = SymbolicInput(name, sort, width)
83
+ item.validate()
84
+ result.append(item)
85
+ return tuple(result)
86
+
87
+
88
+ def _value(value: Any) -> Any:
89
+ if isinstance(value, (str, int, float, bool)) or value is None:
90
+ return value
91
+ if z3.is_expr(value):
92
+ return str(value)
93
+ if isinstance(value, tuple):
94
+ return [_value(item) for item in value]
95
+ if isinstance(value, list):
96
+ return [_value(item) for item in value]
97
+ if isinstance(value, dict):
98
+ return {str(key): _value(item) for key, item in value.items()}
99
+ return repr(value)
100
+
101
+
102
+ def _result_payload(result) -> dict[str, Any]:
103
+ return {
104
+ "schema_version": 1,
105
+ "status": result.status.value,
106
+ "explored_paths": result.explored_paths,
107
+ "pruned_paths": result.pruned_paths,
108
+ "diagnostic": result.diagnostic,
109
+ "paths": [
110
+ {
111
+ "path_id": path.path_id,
112
+ "status": path.status.value,
113
+ "constraints": [str(item) for item in path.constraints],
114
+ "model": None if path.model is None else {key: _value(value) for key, value in path.model.items()},
115
+ "returns": [_value(value) for value in path.returns],
116
+ "raised": _value(path.raised),
117
+ "blocks": list(path.blocks),
118
+ "edges": list(path.edges),
119
+ "steps": path.steps,
120
+ "diagnostic": path.diagnostic,
121
+ }
122
+ for path in result.paths
123
+ ],
124
+ }
125
+
126
+
127
+ def _text_result(result) -> str:
128
+ lines = [
129
+ f"{result.status.value}: {result.explored_paths} explored, {result.pruned_paths} pruned",
130
+ ]
131
+ if result.diagnostic:
132
+ lines.append(result.diagnostic)
133
+ for path in result.paths:
134
+ outcome = path.diagnostic or repr(path.raised if path.raised is not None else path.returns)
135
+ lines.append(f"[{path.path_id}] {path.status.value}; {path.steps} steps; {outcome}")
136
+ if path.constraints:
137
+ lines.append(" constraints: " + "; ".join(str(item) for item in path.constraints))
138
+ if path.model:
139
+ lines.append(" model: " + json.dumps({key: _value(value) for key, value in path.model.items()}, sort_keys=True))
140
+ return "\n".join(lines)
141
+
142
+
143
+ def _exit_code(status: SymbolicStatus) -> int:
144
+ if status is SymbolicStatus.COMPLETED:
145
+ return 0
146
+ if status is SymbolicStatus.INVALID_REQUEST:
147
+ return 2
148
+ return 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unidecompiler-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Command-line host for unidecompiler plugins
5
5
  Author-email: Wker <1670133844@qq.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -11,7 +11,8 @@ Requires-Python: >=3.11
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: unidecompiler<0.3.0,>=0.2.2
13
13
  Requires-Dist: unidecompiler-export<0.3.0,>=0.2.2
14
- Requires-Dist: unidecompiler-simulator<0.3.0,>=0.2.2
14
+ Requires-Dist: unidecompiler-simulator<0.3.0,>=0.2.3
15
+ Requires-Dist: unidecompiler-symbolic<0.3.0,>=0.2.3
15
16
  Requires-Dist: unidecompiler-simulation-host-python<0.3.0,>=0.2.2
16
17
 
17
18
  # unidecompiler-cli
@@ -111,6 +112,23 @@ separate simulator library:
111
112
  unidecompiler simulate sample.bytecode --function 'Example.run' --args '[1, 2]'
112
113
  ```
113
114
 
115
+ Bounded symbolic execution uses the same opaque frontend query and explores
116
+ the recovered generic IR only. `--symbolic` names scalar symbolic parameters;
117
+ all other parameters are supplied through `--concrete`:
118
+
119
+ ```sh
120
+ unidecompiler symbolic sample.pyc --function add \
121
+ --symbolic '{"left":{"sort":"int"}}' --concrete '{"right":5}'
122
+ ```
123
+
124
+ Use `--format text` for a human-readable summary. Exploration is bounded by
125
+ `--max-paths`, `--max-steps`, `--max-loop-unroll`, `--max-call-depth`, and
126
+ `--solver-timeout-ms`; symbolic sorts are `bool`, `int`, `real`, and `bitvec`
127
+ with a positive `bit_width`. JSON output contains each path's constraints,
128
+ model, return/raise value, and CFG trace. Non-success statuses such as
129
+ `unsupported`, `solver_timeout`, limits, `cancelled`, and `invalid_request`
130
+ are returned explicitly and produce a non-zero exit code.
131
+
114
132
  For trusted programs that require functions outside the lifted module, pass a
115
133
  Python environment file. Top-level functions are matched by name, while their
116
134
  stdout and stderr are returned as structured simulation events:
@@ -4,6 +4,7 @@ src/unidecompiler_cli/__init__.py
4
4
  src/unidecompiler_cli/cli.py
5
5
  src/unidecompiler_cli/input_sources.py
6
6
  src/unidecompiler_cli/simulation.py
7
+ src/unidecompiler_cli/symbolic.py
7
8
  src/unidecompiler_cli/templates.py
8
9
  src/unidecompiler_cli.egg-info/PKG-INFO
9
10
  src/unidecompiler_cli.egg-info/SOURCES.txt
@@ -1,4 +1,5 @@
1
1
  unidecompiler<0.3.0,>=0.2.2
2
2
  unidecompiler-export<0.3.0,>=0.2.2
3
- unidecompiler-simulator<0.3.0,>=0.2.2
3
+ unidecompiler-simulator<0.3.0,>=0.2.3
4
+ unidecompiler-symbolic<0.3.0,>=0.2.3
4
5
  unidecompiler-simulation-host-python<0.3.0,>=0.2.2