zexus 1.7.2 → 1.8.1
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.
- package/README.md +57 -6
- package/package.json +2 -1
- package/rust_core/Cargo.lock +603 -0
- package/rust_core/Cargo.toml +26 -0
- package/rust_core/README.md +15 -0
- package/rust_core/pyproject.toml +25 -0
- package/rust_core/src/binary_bytecode.rs +543 -0
- package/rust_core/src/contract_vm.rs +643 -0
- package/rust_core/src/executor.rs +847 -0
- package/rust_core/src/hasher.rs +90 -0
- package/rust_core/src/lib.rs +71 -0
- package/rust_core/src/merkle.rs +128 -0
- package/rust_core/src/rust_vm.rs +2313 -0
- package/rust_core/src/signature.rs +79 -0
- package/rust_core/src/state_adapter.rs +281 -0
- package/rust_core/src/validator.rs +116 -0
- package/scripts/postinstall.js +34 -2
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/blockchain/accelerator.py +27 -0
- package/src/zexus/blockchain/contract_vm.py +409 -3
- package/src/zexus/blockchain/rust_bridge.py +64 -0
- package/src/zexus/cli/main.py +1 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/evaluator/bytecode_compiler.py +150 -52
- package/src/zexus/evaluator/core.py +151 -809
- package/src/zexus/evaluator/expressions.py +27 -22
- package/src/zexus/evaluator/functions.py +171 -126
- package/src/zexus/evaluator/statements.py +55 -112
- package/src/zexus/module_cache.py +20 -9
- package/src/zexus/object.py +330 -38
- package/src/zexus/parser/parser.py +69 -14
- package/src/zexus/parser/strategy_context.py +228 -5
- package/src/zexus/parser/strategy_structural.py +2 -2
- package/src/zexus/persistence.py +46 -17
- package/src/zexus/security.py +140 -234
- package/src/zexus/type_checker.py +44 -5
- package/src/zexus/vm/binary_bytecode.py +7 -3
- package/src/zexus/vm/bytecode.py +6 -0
- package/src/zexus/vm/cache.py +24 -46
- package/src/zexus/vm/compiler.py +80 -20
- package/src/zexus/vm/fastops.c +1093 -2975
- package/src/zexus/vm/gas_metering.py +2 -2
- package/src/zexus/vm/memory_pool.py +21 -9
- package/src/zexus/vm/vm.py +527 -67
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +79 -12
- package/src/zexus.egg-info/SOURCES.txt +23 -1
- package/src/zexus.egg-info/requires.txt +26 -0
- package/src/zexus.egg-info/entry_points.txt +0 -4
package/src/zexus/vm/vm.py
CHANGED
|
@@ -136,6 +136,17 @@ except Exception:
|
|
|
136
136
|
_FASTOPS_AVAILABLE = False
|
|
137
137
|
_fastops = None
|
|
138
138
|
|
|
139
|
+
# Rust VM (Phase 3 — adaptive execution)
|
|
140
|
+
try:
|
|
141
|
+
from zexus_core import RustVMExecutor as _RustVMExecutor
|
|
142
|
+
_RUST_VM_AVAILABLE = True
|
|
143
|
+
except Exception:
|
|
144
|
+
_RUST_VM_AVAILABLE = False
|
|
145
|
+
_RustVMExecutor = None
|
|
146
|
+
|
|
147
|
+
# Sentinel returned when Rust VM signals needs_fallback
|
|
148
|
+
_RUST_VM_FALLBACK_SENTINEL = object()
|
|
149
|
+
|
|
139
150
|
# Async Optimizer (Phase 8)
|
|
140
151
|
try:
|
|
141
152
|
from .async_optimizer import AsyncOptimizer, AsyncOptimizationLevel
|
|
@@ -425,6 +436,28 @@ class VM:
|
|
|
425
436
|
self._in_execution = 0
|
|
426
437
|
self._native_jit_auto_enabled = False
|
|
427
438
|
self._native_jit_auto_threshold = 700
|
|
439
|
+
# SECURITY (H9): Call-depth tracking to prevent unbounded recursion
|
|
440
|
+
self._call_depth = 0
|
|
441
|
+
self._MAX_CALL_DEPTH = 256
|
|
442
|
+
|
|
443
|
+
# --- Rust VM Adaptive Routing (Phase 3 + Phase 6) ---
|
|
444
|
+
self._rust_vm_available = _RUST_VM_AVAILABLE
|
|
445
|
+
self._rust_vm_executor = _RustVMExecutor() if _RUST_VM_AVAILABLE else None
|
|
446
|
+
self._rust_vm_threshold = 0 # Phase 6: route ALL programs through Rust by default
|
|
447
|
+
try:
|
|
448
|
+
_env_thresh = os.environ.get("ZEXUS_RUST_VM_THRESHOLD")
|
|
449
|
+
if _env_thresh is not None:
|
|
450
|
+
self._rust_vm_threshold = int(_env_thresh)
|
|
451
|
+
except (ValueError, TypeError):
|
|
452
|
+
pass
|
|
453
|
+
self._rust_vm_enabled = self._rust_vm_available # Can be disabled at runtime
|
|
454
|
+
self._rust_vm_stats = {
|
|
455
|
+
"rust_executions": 0,
|
|
456
|
+
"rust_fallbacks": 0,
|
|
457
|
+
"python_executions": 0,
|
|
458
|
+
"total_rust_ops": 0,
|
|
459
|
+
}
|
|
460
|
+
|
|
428
461
|
self._env_version = 0
|
|
429
462
|
self._name_cache: Dict[str, Tuple[Any, int]] = {}
|
|
430
463
|
self._method_cache: Dict[Tuple[type, str], Any] = {}
|
|
@@ -637,6 +670,22 @@ class VM:
|
|
|
637
670
|
"""Return a child VM to the pool for reuse."""
|
|
638
671
|
if hasattr(self, "_vm_pool") and self._vm_pool is not None:
|
|
639
672
|
if len(self._vm_pool) < 1000:
|
|
673
|
+
# MEDIUM (M14): Scrub references that could leak state across pooled VMs.
|
|
674
|
+
try:
|
|
675
|
+
vm.env = None
|
|
676
|
+
except Exception:
|
|
677
|
+
pass
|
|
678
|
+
for attr in ("_closure_cells", "_name_cache", "_method_cache", "_events", "_tasks"):
|
|
679
|
+
try:
|
|
680
|
+
val = getattr(vm, attr, None)
|
|
681
|
+
if isinstance(val, dict):
|
|
682
|
+
val.clear()
|
|
683
|
+
except Exception:
|
|
684
|
+
pass
|
|
685
|
+
try:
|
|
686
|
+
vm._parent_env = None
|
|
687
|
+
except Exception:
|
|
688
|
+
pass
|
|
640
689
|
self._vm_pool.append(vm)
|
|
641
690
|
|
|
642
691
|
@classmethod
|
|
@@ -1117,17 +1166,29 @@ class VM:
|
|
|
1117
1166
|
self._bump_env_version(key, value)
|
|
1118
1167
|
return module_env
|
|
1119
1168
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1169
|
+
# SECURITY (H6): Only allow safe Python stdlib modules via importlib.
|
|
1170
|
+
# Dangerous modules (os, subprocess, socket, ctypes, etc.) are blocked.
|
|
1171
|
+
_SAFE_PYTHON_MODULES = frozenset({
|
|
1172
|
+
'math', 'json', 'hashlib', 'base64', 'collections', 'datetime',
|
|
1173
|
+
'decimal', 'fractions', 'itertools', 'functools', 'operator',
|
|
1174
|
+
'string', 'textwrap', 'unicodedata', 'enum', 'dataclasses',
|
|
1175
|
+
'copy', 'pprint', 'reprlib', 'numbers', 'cmath', 'statistics',
|
|
1176
|
+
'random', 'secrets', 'uuid', 're', 'struct', 'binascii',
|
|
1177
|
+
'html', 'urllib.parse', 'ipaddress', 'typing', 'abc',
|
|
1178
|
+
})
|
|
1179
|
+
if module_path in _SAFE_PYTHON_MODULES:
|
|
1180
|
+
try:
|
|
1181
|
+
mod = importlib.import_module(module_path)
|
|
1182
|
+
key = alias or module_path
|
|
1183
|
+
self.env[key] = mod
|
|
1184
|
+
self._bump_env_version(key, mod)
|
|
1185
|
+
return mod
|
|
1186
|
+
except Exception:
|
|
1187
|
+
pass
|
|
1188
|
+
key = alias or module_path
|
|
1189
|
+
self.env[key] = None
|
|
1190
|
+
self._bump_env_version(key, None)
|
|
1191
|
+
return None
|
|
1131
1192
|
|
|
1132
1193
|
def _get_cached_method(self, target: Any, method_name: str):
|
|
1133
1194
|
if target is None:
|
|
@@ -1225,7 +1286,23 @@ class VM:
|
|
|
1225
1286
|
pct = (count / total_ops * 100) if total_ops else 0.0
|
|
1226
1287
|
print(f"[VM PROFILE] {op_name} count={count} pct={pct:.2f}%")
|
|
1227
1288
|
return result
|
|
1228
|
-
|
|
1289
|
+
except Exception:
|
|
1290
|
+
# LOGIC (L4): If an exception escapes a TX block, revert state to snapshot.
|
|
1291
|
+
if self.env.get("_in_transaction", False):
|
|
1292
|
+
try:
|
|
1293
|
+
self.env["_blockchain_state"] = dict(self.env.get("_tx_snapshot", {}))
|
|
1294
|
+
self.env["_tx_pending_state"] = {}
|
|
1295
|
+
self.env.pop("_tx_snapshot", None)
|
|
1296
|
+
if self.use_memory_manager and "_tx_memory_snapshot" in self.env:
|
|
1297
|
+
self._managed_objects = dict(self.env["_tx_memory_snapshot"])
|
|
1298
|
+
del self.env["_tx_memory_snapshot"]
|
|
1299
|
+
tx_stack = self.env.get("_tx_stack", [])
|
|
1300
|
+
if tx_stack:
|
|
1301
|
+
tx_stack.pop()
|
|
1302
|
+
self.env["_in_transaction"] = bool(tx_stack)
|
|
1303
|
+
except Exception:
|
|
1304
|
+
pass
|
|
1305
|
+
raise
|
|
1229
1306
|
finally:
|
|
1230
1307
|
self._in_execution = max(0, getattr(self, "_in_execution", 1) - 1)
|
|
1231
1308
|
self._total_execution_time += (time.perf_counter() - start_time)
|
|
@@ -1505,12 +1582,9 @@ class VM:
|
|
|
1505
1582
|
return {'jit_enabled': False}
|
|
1506
1583
|
|
|
1507
1584
|
def _ensure_recursion_headroom(self, minimum: int = 5000):
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
sys.setrecursionlimit(minimum)
|
|
1512
|
-
except Exception:
|
|
1513
|
-
pass
|
|
1585
|
+
# SECURITY (H13): No longer increases sys.setrecursionlimit.
|
|
1586
|
+
# VM-level call depth is enforced by _call_depth / _MAX_CALL_DEPTH instead.
|
|
1587
|
+
pass
|
|
1514
1588
|
|
|
1515
1589
|
def clear_jit_cache(self):
|
|
1516
1590
|
if self.use_jit and self.jit_compiler:
|
|
@@ -1568,25 +1642,13 @@ class VM:
|
|
|
1568
1642
|
del self._managed_objects[name]
|
|
1569
1643
|
return {'collected': collected, 'gc_time': gc_time}
|
|
1570
1644
|
|
|
1571
|
-
# Fallback:
|
|
1572
|
-
#
|
|
1645
|
+
# Fallback: Without a memory manager, do NOT delete user variables.
|
|
1646
|
+
# Deleting non-underscore env keys breaks program state and can silently
|
|
1647
|
+
# corrupt execution. Keep this as a no-op to preserve semantics.
|
|
1573
1648
|
if force:
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
for key in list(self.env.keys()):
|
|
1578
|
-
# Don't remove special keys or builtins
|
|
1579
|
-
if not key.startswith('_') and key not in self.builtins:
|
|
1580
|
-
keys_to_remove.append(key)
|
|
1581
|
-
|
|
1582
|
-
# Remove temporary variables
|
|
1583
|
-
for key in keys_to_remove:
|
|
1584
|
-
del self.env[key]
|
|
1585
|
-
|
|
1586
|
-
cleared = initial_count - len(self.env)
|
|
1587
|
-
return {'collected': cleared, 'message': 'Environment variables cleared'}
|
|
1588
|
-
|
|
1589
|
-
return {'collected': 0, 'message': 'Memory manager disabled or not forced'}
|
|
1649
|
+
return {'collected': 0, 'message': 'No-op: memory manager disabled (force ignored)'}
|
|
1650
|
+
|
|
1651
|
+
return {'collected': 0, 'message': 'Memory manager disabled'}
|
|
1590
1652
|
|
|
1591
1653
|
|
|
1592
1654
|
def _allocate_managed(self, value: Any, name: str = None, root: bool = False) -> int:
|
|
@@ -1704,6 +1766,126 @@ class VM:
|
|
|
1704
1766
|
if tag == "LIST": return [self._eval_hl_op(e) for e in op[1]]
|
|
1705
1767
|
return None
|
|
1706
1768
|
|
|
1769
|
+
# ==================== Rust VM Adaptive Routing (Phase 3) ====================
|
|
1770
|
+
|
|
1771
|
+
def _execute_via_rust_vm(self, bytecode, debug=False):
|
|
1772
|
+
"""Serialize bytecode to .zxc and execute via the Rust VM.
|
|
1773
|
+
|
|
1774
|
+
Returns the result value on success, or ``_RUST_VM_FALLBACK_SENTINEL``
|
|
1775
|
+
if the Rust VM signals it needs a Python fallback (e.g. for
|
|
1776
|
+
CALL_NAME / CALL_METHOD that need Python interop).
|
|
1777
|
+
"""
|
|
1778
|
+
from .binary_bytecode import serialize as _serialize_zxc
|
|
1779
|
+
|
|
1780
|
+
# Serialize bytecode → .zxc binary
|
|
1781
|
+
zxc_data = _serialize_zxc(bytecode, include_checksum=True)
|
|
1782
|
+
|
|
1783
|
+
# Build env dict for Rust (only simple serializable values)
|
|
1784
|
+
rust_env = {}
|
|
1785
|
+
for k, v in self.env.items():
|
|
1786
|
+
if isinstance(v, (int, float, str, bool, type(None))):
|
|
1787
|
+
rust_env[k] = v
|
|
1788
|
+
elif isinstance(v, ZInteger):
|
|
1789
|
+
rust_env[k] = v.value
|
|
1790
|
+
elif isinstance(v, ZFloat):
|
|
1791
|
+
rust_env[k] = v.value
|
|
1792
|
+
elif isinstance(v, ZString):
|
|
1793
|
+
rust_env[k] = v.value
|
|
1794
|
+
elif isinstance(v, ZBoolean):
|
|
1795
|
+
rust_env[k] = v.value
|
|
1796
|
+
elif isinstance(v, (ZNull, type(None))):
|
|
1797
|
+
rust_env[k] = None
|
|
1798
|
+
# Skip non-serializable values (callables, AST nodes, etc.)
|
|
1799
|
+
|
|
1800
|
+
# Build state dict (if blockchain state exists)
|
|
1801
|
+
rust_state = {}
|
|
1802
|
+
bc_state = self.env.get("_blockchain_state")
|
|
1803
|
+
if bc_state and isinstance(bc_state, dict):
|
|
1804
|
+
for k, v in bc_state.items():
|
|
1805
|
+
if isinstance(v, (int, float, str, bool, type(None))):
|
|
1806
|
+
rust_state[k] = v
|
|
1807
|
+
|
|
1808
|
+
# Gas limit
|
|
1809
|
+
gas_limit = 0
|
|
1810
|
+
if self.gas_metering:
|
|
1811
|
+
remaining_fn = getattr(self.gas_metering, "remaining", None)
|
|
1812
|
+
if callable(remaining_fn):
|
|
1813
|
+
try:
|
|
1814
|
+
rem = remaining_fn()
|
|
1815
|
+
if isinstance(rem, (int, float)) and rem > 0:
|
|
1816
|
+
gas_limit = int(rem)
|
|
1817
|
+
except Exception:
|
|
1818
|
+
pass
|
|
1819
|
+
if gas_limit == 0:
|
|
1820
|
+
gas_limit_val = getattr(self.gas_metering, "gas_limit", 0) or 0
|
|
1821
|
+
gas_used_val = getattr(self.gas_metering, "gas_used", 0) or 0
|
|
1822
|
+
if isinstance(gas_limit_val, (int, float)) and isinstance(gas_used_val, (int, float)):
|
|
1823
|
+
if gas_limit_val > gas_used_val:
|
|
1824
|
+
gas_limit = int(gas_limit_val - gas_used_val)
|
|
1825
|
+
|
|
1826
|
+
# Execute via Rust VM
|
|
1827
|
+
result_dict = self._rust_vm_executor.execute(
|
|
1828
|
+
zxc_data,
|
|
1829
|
+
env=rust_env or None,
|
|
1830
|
+
state=rust_state or None,
|
|
1831
|
+
gas_limit=gas_limit,
|
|
1832
|
+
)
|
|
1833
|
+
|
|
1834
|
+
# Check for fallback
|
|
1835
|
+
if result_dict.get("needs_fallback", False):
|
|
1836
|
+
self._rust_vm_stats["rust_fallbacks"] += 1
|
|
1837
|
+
if debug:
|
|
1838
|
+
print("[VM] Rust VM needs Python fallback — delegating to Python VM")
|
|
1839
|
+
return _RUST_VM_FALLBACK_SENTINEL
|
|
1840
|
+
|
|
1841
|
+
# Check for errors
|
|
1842
|
+
error = result_dict.get("error")
|
|
1843
|
+
if error:
|
|
1844
|
+
if "OutOfGas" in str(error):
|
|
1845
|
+
if self.gas_metering:
|
|
1846
|
+
raise OutOfGasError(str(error))
|
|
1847
|
+
raise RuntimeError(str(error))
|
|
1848
|
+
if "RequireFailed" in str(error):
|
|
1849
|
+
raise RuntimeError(str(error))
|
|
1850
|
+
raise RuntimeError(f"Rust VM error: {error}")
|
|
1851
|
+
|
|
1852
|
+
# Success — update stats
|
|
1853
|
+
self._rust_vm_stats["rust_executions"] += 1
|
|
1854
|
+
self._rust_vm_stats["total_rust_ops"] += result_dict.get("instructions_executed", 0)
|
|
1855
|
+
|
|
1856
|
+
# Bridge gas usage back to Python metering
|
|
1857
|
+
if self.gas_metering:
|
|
1858
|
+
rust_gas = result_dict.get("gas_used", 0)
|
|
1859
|
+
if rust_gas > 0:
|
|
1860
|
+
gas_used_attr = getattr(self.gas_metering, "gas_used", None)
|
|
1861
|
+
if gas_used_attr is not None:
|
|
1862
|
+
self.gas_metering.gas_used = gas_used_attr + rust_gas
|
|
1863
|
+
add_gas = getattr(self.gas_metering, "add_gas", None)
|
|
1864
|
+
if add_gas:
|
|
1865
|
+
add_gas(rust_gas)
|
|
1866
|
+
|
|
1867
|
+
# Merge Rust env back into Python env
|
|
1868
|
+
rust_env_out = result_dict.get("env", {})
|
|
1869
|
+
if rust_env_out:
|
|
1870
|
+
for k, v in rust_env_out.items():
|
|
1871
|
+
self.env[k] = v
|
|
1872
|
+
|
|
1873
|
+
# Merge blockchain state back
|
|
1874
|
+
rust_state_out = result_dict.get("state", {})
|
|
1875
|
+
if rust_state_out and bc_state is not None:
|
|
1876
|
+
for k, v in rust_state_out.items():
|
|
1877
|
+
bc_state[k] = v
|
|
1878
|
+
|
|
1879
|
+
if debug:
|
|
1880
|
+
print(f"[VM] Rust VM executed: ops={result_dict.get('instructions_executed', 0)} "
|
|
1881
|
+
f"gas={result_dict.get('gas_used', 0)}")
|
|
1882
|
+
|
|
1883
|
+
return result_dict.get("result")
|
|
1884
|
+
|
|
1885
|
+
def get_rust_vm_stats(self):
|
|
1886
|
+
"""Return statistics about Rust VM usage."""
|
|
1887
|
+
return dict(self._rust_vm_stats)
|
|
1888
|
+
|
|
1707
1889
|
# ==================== Fast Synchronous Dispatch (Performance Mode) ====================
|
|
1708
1890
|
|
|
1709
1891
|
def _run_stack_bytecode_sync(self, bytecode, debug=False):
|
|
@@ -1736,13 +1918,28 @@ class VM:
|
|
|
1736
1918
|
except Exception:
|
|
1737
1919
|
pass
|
|
1738
1920
|
|
|
1921
|
+
# Rust VM adaptive routing (Phase 3) — delegate to Rust when
|
|
1922
|
+
# the program is large enough to amortise serialisation overhead.
|
|
1923
|
+
if (self._rust_vm_enabled
|
|
1924
|
+
and self._rust_vm_executor is not None
|
|
1925
|
+
and len(instrs) >= self._rust_vm_threshold):
|
|
1926
|
+
try:
|
|
1927
|
+
rust_result = self._execute_via_rust_vm(bytecode, debug)
|
|
1928
|
+
if rust_result is not _RUST_VM_FALLBACK_SENTINEL:
|
|
1929
|
+
return rust_result
|
|
1930
|
+
# Rust signalled needs_fallback — continue to Python VM
|
|
1931
|
+
except Exception:
|
|
1932
|
+
self._rust_vm_stats["rust_fallbacks"] += 1
|
|
1933
|
+
|
|
1739
1934
|
# Fast stack implementation
|
|
1740
1935
|
stack: List[Any] = []
|
|
1741
1936
|
stack_append = stack.append
|
|
1742
1937
|
# stack_pop = stack.pop
|
|
1743
1938
|
def stack_pop():
|
|
1939
|
+
# MEDIUM (M15): Match the main VM stack behaviour; stack underflow
|
|
1940
|
+
# should not silently return None.
|
|
1744
1941
|
if not stack:
|
|
1745
|
-
|
|
1942
|
+
raise IndexError("pop from empty stack")
|
|
1746
1943
|
return stack.pop()
|
|
1747
1944
|
|
|
1748
1945
|
ip = 0
|
|
@@ -1854,7 +2051,19 @@ class VM:
|
|
|
1854
2051
|
elif op_name == "MOD":
|
|
1855
2052
|
b = stack_pop() if stack else 1
|
|
1856
2053
|
a = stack_pop() if stack else 0
|
|
2054
|
+
if hasattr(a, 'value'): a = a.value
|
|
2055
|
+
if hasattr(b, 'value'): b = b.value
|
|
2056
|
+
if a is None: a = 0
|
|
2057
|
+
if b is None: b = 1
|
|
1857
2058
|
stack_append(a % b if b != 0 else 0)
|
|
2059
|
+
elif op_name == "POW":
|
|
2060
|
+
b = stack_pop() if stack else 1
|
|
2061
|
+
a = stack_pop() if stack else 0
|
|
2062
|
+
if hasattr(a, 'value'): a = a.value
|
|
2063
|
+
if hasattr(b, 'value'): b = b.value
|
|
2064
|
+
if a is None: a = 0
|
|
2065
|
+
if b is None: b = 0
|
|
2066
|
+
stack_append(_safe_pow(a, b))
|
|
1858
2067
|
elif op_name == "EQ":
|
|
1859
2068
|
b = stack_pop() if stack else None
|
|
1860
2069
|
a = stack_pop() if stack else None
|
|
@@ -1886,6 +2095,18 @@ class VM:
|
|
|
1886
2095
|
elif op_name == "NOT":
|
|
1887
2096
|
a = stack_pop() if stack else False
|
|
1888
2097
|
stack_append(not a)
|
|
2098
|
+
elif op_name == "AND":
|
|
2099
|
+
b = stack_pop() if stack else False
|
|
2100
|
+
a = stack_pop() if stack else False
|
|
2101
|
+
if hasattr(a, 'value'): a = a.value
|
|
2102
|
+
if hasattr(b, 'value'): b = b.value
|
|
2103
|
+
stack_append(bool(a) and bool(b))
|
|
2104
|
+
elif op_name == "OR":
|
|
2105
|
+
b = stack_pop() if stack else False
|
|
2106
|
+
a = stack_pop() if stack else False
|
|
2107
|
+
if hasattr(a, 'value'): a = a.value
|
|
2108
|
+
if hasattr(b, 'value'): b = b.value
|
|
2109
|
+
stack_append(bool(a) or bool(b))
|
|
1889
2110
|
elif op_name == "NEG":
|
|
1890
2111
|
a = stack_pop() if stack else 0
|
|
1891
2112
|
stack_append(-a)
|
|
@@ -1895,6 +2116,52 @@ class VM:
|
|
|
1895
2116
|
cond = stack_pop() if stack else None
|
|
1896
2117
|
if not cond:
|
|
1897
2118
|
ip = operand
|
|
2119
|
+
elif op_name == "JUMP_IF_TRUE":
|
|
2120
|
+
cond = stack_pop() if stack else None
|
|
2121
|
+
if hasattr(cond, 'value'):
|
|
2122
|
+
cond = cond.value
|
|
2123
|
+
if cond:
|
|
2124
|
+
ip = operand
|
|
2125
|
+
elif op_name == "IMPORT":
|
|
2126
|
+
# Mirror async IMPORT behavior (minimal)
|
|
2127
|
+
try:
|
|
2128
|
+
if isinstance(operand, (list, tuple)) and operand:
|
|
2129
|
+
mod_name = const(operand[0])
|
|
2130
|
+
alias = const(operand[1]) if len(operand) > 1 else ""
|
|
2131
|
+
names = const(operand[2]) if len(operand) > 2 else []
|
|
2132
|
+
is_named = const(operand[3]) if len(operand) > 3 else False
|
|
2133
|
+
else:
|
|
2134
|
+
mod_name = const(operand)
|
|
2135
|
+
alias, names, is_named = "", [], False
|
|
2136
|
+
self._execute_import(mod_name, alias=alias or "", names=names, is_named=bool(is_named))
|
|
2137
|
+
except Exception:
|
|
2138
|
+
pass
|
|
2139
|
+
elif op_name == "EXPORT":
|
|
2140
|
+
# vm/compiler.py emits: LOAD_NAME <name_idx>; EXPORT <name_idx>
|
|
2141
|
+
try:
|
|
2142
|
+
if isinstance(operand, int):
|
|
2143
|
+
name = const(operand)
|
|
2144
|
+
value = stack_pop() if stack else None
|
|
2145
|
+
elif isinstance(operand, (list, tuple)) and operand:
|
|
2146
|
+
name = const(operand[0])
|
|
2147
|
+
value = stack_pop() if stack else None
|
|
2148
|
+
if len(operand) > 1:
|
|
2149
|
+
value = const(operand[1])
|
|
2150
|
+
else:
|
|
2151
|
+
name = stack_pop() if stack else None
|
|
2152
|
+
value = stack_pop() if stack else None
|
|
2153
|
+
|
|
2154
|
+
export_fn = getattr(self.env, "export", None)
|
|
2155
|
+
if callable(export_fn):
|
|
2156
|
+
try:
|
|
2157
|
+
export_fn(name, value)
|
|
2158
|
+
except Exception:
|
|
2159
|
+
pass
|
|
2160
|
+
else:
|
|
2161
|
+
self.env[name] = value
|
|
2162
|
+
self._bump_env_version(name, value)
|
|
2163
|
+
except Exception:
|
|
2164
|
+
pass
|
|
1898
2165
|
elif op_name == "RETURN":
|
|
1899
2166
|
return stack_pop() if stack else None
|
|
1900
2167
|
elif op_name == "BUILD_LIST":
|
|
@@ -2040,7 +2307,17 @@ class VM:
|
|
|
2040
2307
|
result = attr(*args)
|
|
2041
2308
|
elif isinstance(target, dict) and method_name in target:
|
|
2042
2309
|
candidate = target[method_name]
|
|
2043
|
-
|
|
2310
|
+
# Handle Builtin objects (have .fn), ZAction/ZLambda (have .call),
|
|
2311
|
+
# and regular callables
|
|
2312
|
+
if callable(candidate):
|
|
2313
|
+
result = candidate(*args)
|
|
2314
|
+
elif hasattr(candidate, 'fn') and callable(candidate.fn):
|
|
2315
|
+
# Builtin from evaluator — call inner fn
|
|
2316
|
+
result = candidate.fn(*args)
|
|
2317
|
+
elif hasattr(candidate, 'call') and callable(candidate.call):
|
|
2318
|
+
result = candidate.call(*args)
|
|
2319
|
+
else:
|
|
2320
|
+
result = candidate
|
|
2044
2321
|
else:
|
|
2045
2322
|
result = attr
|
|
2046
2323
|
except Exception:
|
|
@@ -2231,6 +2508,7 @@ class VM:
|
|
|
2231
2508
|
elif op_name == "LEDGER_APPEND":
|
|
2232
2509
|
entry = stack_pop()
|
|
2233
2510
|
ledger = env.setdefault("_ledger", [])
|
|
2511
|
+
# MEDIUM (M12): Keep ledger bounded to prevent memory exhaustion.
|
|
2234
2512
|
if len(ledger) < 10000: # Size limit
|
|
2235
2513
|
if isinstance(entry, dict) and "timestamp" not in entry:
|
|
2236
2514
|
entry["timestamp"] = time.time()
|
|
@@ -2266,8 +2544,16 @@ class VM:
|
|
|
2266
2544
|
data = stack_pop(); action = stack_pop()
|
|
2267
2545
|
if hasattr(action, 'value'): action = action.value
|
|
2268
2546
|
if hasattr(data, 'value'): data = data.value
|
|
2269
|
-
|
|
2270
|
-
|
|
2547
|
+
# MEDIUM (M13): Cap audit log growth to prevent memory exhaustion.
|
|
2548
|
+
audit = env.setdefault("_audit_log", [])
|
|
2549
|
+
entry = {"timestamp": ts, "action": action, "data": data}
|
|
2550
|
+
if len(audit) >= 10000:
|
|
2551
|
+
# Drop oldest entries; keep most recent.
|
|
2552
|
+
try:
|
|
2553
|
+
del audit[:1000]
|
|
2554
|
+
except Exception:
|
|
2555
|
+
audit[:] = audit[-9000:]
|
|
2556
|
+
audit.append(entry)
|
|
2271
2557
|
|
|
2272
2558
|
elif op_name == "RESTRICT_ACCESS":
|
|
2273
2559
|
restriction = stack_pop(); prop = stack_pop(); obj = stack_pop()
|
|
@@ -2406,6 +2692,20 @@ class VM:
|
|
|
2406
2692
|
"""Synchronous callable invocation for fast dispatch."""
|
|
2407
2693
|
if fn is None:
|
|
2408
2694
|
return None
|
|
2695
|
+
# SECURITY (H9): Enforce call-depth limit
|
|
2696
|
+
if self._call_depth >= self._MAX_CALL_DEPTH:
|
|
2697
|
+
raise RecursionError(
|
|
2698
|
+
f"VM call depth exceeded maximum ({self._MAX_CALL_DEPTH}). "
|
|
2699
|
+
"Possible infinite recursion."
|
|
2700
|
+
)
|
|
2701
|
+
self._call_depth += 1
|
|
2702
|
+
try:
|
|
2703
|
+
return self._invoke_callable_sync_inner(fn, args)
|
|
2704
|
+
finally:
|
|
2705
|
+
self._call_depth -= 1
|
|
2706
|
+
|
|
2707
|
+
def _invoke_callable_sync_inner(self, fn, args):
|
|
2708
|
+
"""Inner implementation of callable invocation."""
|
|
2409
2709
|
real_fn = fn.fn if hasattr(fn, "fn") else fn
|
|
2410
2710
|
ZAction, ZLambda = _get_action_types()
|
|
2411
2711
|
if ZAction is not None and isinstance(real_fn, (ZAction, ZLambda)):
|
|
@@ -2610,14 +2910,20 @@ class VM:
|
|
|
2610
2910
|
_cached_ZAction, _cached_ZLambda = _get_action_types()
|
|
2611
2911
|
|
|
2612
2912
|
class _EvalStack:
|
|
2613
|
-
__slots__ = ("data", "sp")
|
|
2913
|
+
__slots__ = ("data", "sp", "_max_depth")
|
|
2914
|
+
_MAX_STACK_DEPTH = 100_000 # SECURITY (H8): prevent unbounded stack growth
|
|
2614
2915
|
|
|
2615
2916
|
def __init__(self, capacity: int):
|
|
2616
2917
|
base = max(32, capacity)
|
|
2617
2918
|
self.data = [None] * base
|
|
2618
2919
|
self.sp = 0
|
|
2920
|
+
self._max_depth = _EvalStack._MAX_STACK_DEPTH
|
|
2619
2921
|
|
|
2620
2922
|
def _ensure_capacity(self):
|
|
2923
|
+
if self.sp >= self._max_depth:
|
|
2924
|
+
raise OverflowError(
|
|
2925
|
+
f"VM stack overflow: depth {self.sp} exceeds maximum {self._max_depth}"
|
|
2926
|
+
)
|
|
2621
2927
|
if self.sp >= len(self.data):
|
|
2622
2928
|
self.data.extend([None] * len(self.data))
|
|
2623
2929
|
|
|
@@ -3003,7 +3309,16 @@ class VM:
|
|
|
3003
3309
|
result = attr(*args)
|
|
3004
3310
|
elif isinstance(target, dict) and method_name in target:
|
|
3005
3311
|
candidate = target[method_name]
|
|
3006
|
-
|
|
3312
|
+
# Handle Builtin objects (have .fn), ZAction/ZLambda (have .call),
|
|
3313
|
+
# and regular callables
|
|
3314
|
+
if callable(candidate):
|
|
3315
|
+
result = candidate(*args)
|
|
3316
|
+
elif hasattr(candidate, 'fn') and callable(candidate.fn):
|
|
3317
|
+
result = candidate.fn(*args)
|
|
3318
|
+
elif hasattr(candidate, 'call') and callable(candidate.call):
|
|
3319
|
+
result = candidate.call(*args)
|
|
3320
|
+
else:
|
|
3321
|
+
result = candidate
|
|
3007
3322
|
else:
|
|
3008
3323
|
result = attr
|
|
3009
3324
|
if _verbose_active and self._call_method_trace_count <= 25:
|
|
@@ -3203,13 +3518,33 @@ class VM:
|
|
|
3203
3518
|
def _op_read(_):
|
|
3204
3519
|
path = stack.pop() if stack else None
|
|
3205
3520
|
try:
|
|
3206
|
-
import os
|
|
3207
|
-
if path
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3521
|
+
import os as _os
|
|
3522
|
+
if path:
|
|
3523
|
+
# SECURITY (C11): Sandbox file reads to the module root (env __DIR__) when present.
|
|
3524
|
+
# This keeps sandboxing while allowing tests/runtime to set the project root without
|
|
3525
|
+
# relying on global process CWD.
|
|
3526
|
+
sandbox = _os.getcwd()
|
|
3527
|
+
try:
|
|
3528
|
+
env_dir = env_get("__DIR__", None)
|
|
3529
|
+
if env_dir:
|
|
3530
|
+
sandbox = _os.path.realpath(str(env_dir))
|
|
3531
|
+
except Exception:
|
|
3532
|
+
pass
|
|
3533
|
+
|
|
3534
|
+
if isinstance(path, str) and '\x00' not in path:
|
|
3535
|
+
candidate = path
|
|
3536
|
+
if _os.path.isabs(candidate):
|
|
3537
|
+
resolved = _os.path.realpath(candidate)
|
|
3538
|
+
else:
|
|
3539
|
+
resolved = _os.path.realpath(_os.path.join(sandbox, candidate))
|
|
3540
|
+
|
|
3541
|
+
if resolved == sandbox or resolved.startswith(sandbox + _os.sep):
|
|
3542
|
+
if _os.path.exists(resolved):
|
|
3543
|
+
with open(resolved, 'r') as f:
|
|
3544
|
+
stack.append(f.read())
|
|
3545
|
+
return
|
|
3546
|
+
stack.append(None)
|
|
3547
|
+
except (IOError, OSError, PermissionError):
|
|
3213
3548
|
stack.append(None)
|
|
3214
3549
|
|
|
3215
3550
|
def _op_store_func(operand):
|
|
@@ -3236,7 +3571,7 @@ class VM:
|
|
|
3236
3571
|
"MUL": _binary_op(lambda a, b: a * b),
|
|
3237
3572
|
"DIV": _binary_op(lambda a, b: a / b if b != 0 else 0),
|
|
3238
3573
|
"MOD": _binary_op(lambda a, b: a % b if b != 0 else 0),
|
|
3239
|
-
"POW": _binary_op(lambda a, b: a
|
|
3574
|
+
"POW": _binary_op(lambda a, b: _safe_pow(a, b)),
|
|
3240
3575
|
"NEG": _op_neg,
|
|
3241
3576
|
"EQ": _binary_bool_op(lambda a, b: a == b),
|
|
3242
3577
|
"NEQ": _binary_bool_op(lambda a, b: a != b),
|
|
@@ -3293,6 +3628,7 @@ class VM:
|
|
|
3293
3628
|
(self.enable_fast_loop or auto_fast_loop)
|
|
3294
3629
|
and not profile_ops
|
|
3295
3630
|
and not gas_enabled # Never skip gas metering (including gas_light)
|
|
3631
|
+
and not gas_light # SECURITY (C13): also block fast loop under gas_light
|
|
3296
3632
|
and not trace_interval
|
|
3297
3633
|
and trace_ip_range is None
|
|
3298
3634
|
and not trace_loads_active
|
|
@@ -3309,6 +3645,8 @@ class VM:
|
|
|
3309
3645
|
reasons.append("opcode_profile")
|
|
3310
3646
|
if gas_enabled and not gas_light:
|
|
3311
3647
|
reasons.append("gas")
|
|
3648
|
+
if gas_light:
|
|
3649
|
+
reasons.append("gas_light")
|
|
3312
3650
|
if trace_interval:
|
|
3313
3651
|
reasons.append("trace_interval")
|
|
3314
3652
|
if trace_ip_range is not None:
|
|
@@ -3502,7 +3840,7 @@ class VM:
|
|
|
3502
3840
|
self._jit_registers = {}
|
|
3503
3841
|
self._jit_registers[reg] = value
|
|
3504
3842
|
|
|
3505
|
-
|
|
3843
|
+
res = _safe_pow(v1, v2)
|
|
3506
3844
|
reg, name_idx = operand
|
|
3507
3845
|
name = const(name_idx)
|
|
3508
3846
|
value = _resolve(name)
|
|
@@ -3814,11 +4152,17 @@ class VM:
|
|
|
3814
4152
|
if hasattr(b, 'value'): b = b.value
|
|
3815
4153
|
stack.append(a / b if b != 0 else 0)
|
|
3816
4154
|
elif op_name == "MOD":
|
|
3817
|
-
b = stack.pop() if stack else 1
|
|
4155
|
+
b = _unwrap(stack.pop() if stack else 1)
|
|
4156
|
+
a = _unwrap(stack.pop() if stack else 0)
|
|
4157
|
+
if a is None: a = 0
|
|
4158
|
+
if b is None: b = 1
|
|
3818
4159
|
stack.append(a % b if b != 0 else 0)
|
|
3819
4160
|
elif op_name == "POW":
|
|
3820
|
-
b =
|
|
3821
|
-
stack.
|
|
4161
|
+
b = _unwrap(stack.pop() if stack else 0)
|
|
4162
|
+
a = _unwrap(stack.pop() if stack else 0)
|
|
4163
|
+
if a is None: a = 0
|
|
4164
|
+
if b is None: b = 0
|
|
4165
|
+
stack.append(_safe_pow(a, b))
|
|
3822
4166
|
elif op_name == "NEG":
|
|
3823
4167
|
a = stack.pop() if stack else 0
|
|
3824
4168
|
stack.append(-a)
|
|
@@ -3843,6 +4187,14 @@ class VM:
|
|
|
3843
4187
|
elif op_name == "NOT":
|
|
3844
4188
|
a = stack.pop() if stack else False
|
|
3845
4189
|
stack.append(not a)
|
|
4190
|
+
elif op_name == "AND":
|
|
4191
|
+
b = _unwrap(stack.pop() if stack else False)
|
|
4192
|
+
a = _unwrap(stack.pop() if stack else False)
|
|
4193
|
+
stack.append(bool(a) and bool(b))
|
|
4194
|
+
elif op_name == "OR":
|
|
4195
|
+
b = _unwrap(stack.pop() if stack else False)
|
|
4196
|
+
a = _unwrap(stack.pop() if stack else False)
|
|
4197
|
+
stack.append(bool(a) or bool(b))
|
|
3846
4198
|
|
|
3847
4199
|
# --- Control Flow ---
|
|
3848
4200
|
elif op_name == "JUMP":
|
|
@@ -3850,6 +4202,9 @@ class VM:
|
|
|
3850
4202
|
elif op_name == "JUMP_IF_FALSE":
|
|
3851
4203
|
cond = stack.pop() if stack else None
|
|
3852
4204
|
if not cond: ip = operand
|
|
4205
|
+
elif op_name == "JUMP_IF_TRUE":
|
|
4206
|
+
cond = _unwrap(stack.pop() if stack else None)
|
|
4207
|
+
if cond: ip = operand
|
|
3853
4208
|
elif op_name == "RETURN":
|
|
3854
4209
|
return stack.pop() if stack else None
|
|
3855
4210
|
|
|
@@ -4045,15 +4400,20 @@ class VM:
|
|
|
4045
4400
|
self._execute_import(mod_name, alias=alias or "", names=names, is_named=bool(is_named))
|
|
4046
4401
|
|
|
4047
4402
|
elif op_name == "EXPORT":
|
|
4403
|
+
# vm/compiler.py emits: LOAD_NAME <name_idx>; EXPORT <name_idx>
|
|
4404
|
+
# Support both legacy tuple operand and int operand.
|
|
4048
4405
|
name = None
|
|
4049
4406
|
value = None
|
|
4050
|
-
if isinstance(operand,
|
|
4407
|
+
if isinstance(operand, int):
|
|
4408
|
+
name = const(operand)
|
|
4409
|
+
value = stack.pop() if stack else None
|
|
4410
|
+
elif isinstance(operand, (list, tuple)) and operand:
|
|
4051
4411
|
name = const(operand[0])
|
|
4412
|
+
value = stack.pop() if stack else None
|
|
4052
4413
|
if len(operand) > 1:
|
|
4053
4414
|
value = const(operand[1])
|
|
4054
|
-
|
|
4415
|
+
else:
|
|
4055
4416
|
name = stack.pop() if stack else None
|
|
4056
|
-
if value is None:
|
|
4057
4417
|
value = stack.pop() if stack else None
|
|
4058
4418
|
|
|
4059
4419
|
export_fn = getattr(self.env, "export", None)
|
|
@@ -4071,12 +4431,34 @@ class VM:
|
|
|
4071
4431
|
path = stack.pop() if stack else None
|
|
4072
4432
|
try:
|
|
4073
4433
|
if path is not None:
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4434
|
+
# SECURITY (C12): Sandbox file writes to the module root (env __DIR__) when present.
|
|
4435
|
+
import os as _os_w
|
|
4436
|
+
sandbox = _os_w.getcwd()
|
|
4437
|
+
try:
|
|
4438
|
+
env_dir = self.env.get("__DIR__", None)
|
|
4439
|
+
if env_dir:
|
|
4440
|
+
sandbox = _os_w.path.realpath(str(env_dir))
|
|
4441
|
+
except Exception:
|
|
4442
|
+
pass
|
|
4443
|
+
|
|
4444
|
+
path_str = str(path)
|
|
4445
|
+
if "\x00" in path_str:
|
|
4446
|
+
stack.append(False)
|
|
4447
|
+
continue
|
|
4448
|
+
|
|
4449
|
+
if _os_w.path.isabs(path_str):
|
|
4450
|
+
resolved = _os_w.path.realpath(path_str)
|
|
4451
|
+
else:
|
|
4452
|
+
resolved = _os_w.path.realpath(_os_w.path.join(sandbox, path_str))
|
|
4453
|
+
if not (resolved == sandbox or resolved.startswith(sandbox + _os_w.sep)):
|
|
4454
|
+
stack.append(False) # path traversal denied
|
|
4455
|
+
else:
|
|
4456
|
+
with open(resolved, "w") as f:
|
|
4457
|
+
if isinstance(payload, bytes):
|
|
4458
|
+
f.write(payload.decode("utf-8"))
|
|
4459
|
+
else:
|
|
4460
|
+
f.write(str(payload) if payload is not None else "")
|
|
4461
|
+
stack.append(True)
|
|
4080
4462
|
else:
|
|
4081
4463
|
stack.append(False)
|
|
4082
4464
|
except Exception:
|
|
@@ -4398,7 +4780,13 @@ class VM:
|
|
|
4398
4780
|
data = data.value if hasattr(data, 'value') else data
|
|
4399
4781
|
|
|
4400
4782
|
entry = {"timestamp": ts, "action": action, "data": data}
|
|
4401
|
-
self.env.setdefault("_audit_log", [])
|
|
4783
|
+
audit = self.env.setdefault("_audit_log", [])
|
|
4784
|
+
if len(audit) >= 10000:
|
|
4785
|
+
try:
|
|
4786
|
+
del audit[:1000]
|
|
4787
|
+
except Exception:
|
|
4788
|
+
audit[:] = audit[-9000:]
|
|
4789
|
+
audit.append(entry)
|
|
4402
4790
|
if self.debug: print(f"[AUDIT] {entry}")
|
|
4403
4791
|
|
|
4404
4792
|
elif op_name == "RESTRICT_ACCESS":
|
|
@@ -4443,7 +4831,9 @@ class VM:
|
|
|
4443
4831
|
entry = stack.pop() if stack else None
|
|
4444
4832
|
if isinstance(entry, dict) and "timestamp" not in entry:
|
|
4445
4833
|
entry["timestamp"] = time.time()
|
|
4446
|
-
self.env.setdefault("_ledger", [])
|
|
4834
|
+
ledger = self.env.setdefault("_ledger", [])
|
|
4835
|
+
if len(ledger) < 10000:
|
|
4836
|
+
ledger.append(entry)
|
|
4447
4837
|
|
|
4448
4838
|
elif op_name in ("PARALLEL_START", "PARALLEL_END"):
|
|
4449
4839
|
# Marker ops for parallel execution - no-op in stack VM
|
|
@@ -4462,6 +4852,22 @@ class VM:
|
|
|
4462
4852
|
if debug: print(f"[VM ERROR RECOVERY] {e}")
|
|
4463
4853
|
self.env.setdefault("_errors", []).append(str(e))
|
|
4464
4854
|
else:
|
|
4855
|
+
# LOGIC (L4): If an exception escapes a TX block, revert state to snapshot.
|
|
4856
|
+
if self.env.get("_in_transaction", False):
|
|
4857
|
+
try:
|
|
4858
|
+
self.env["_blockchain_state"] = dict(self.env.get("_tx_snapshot", {}))
|
|
4859
|
+
self.env["_tx_pending_state"] = {}
|
|
4860
|
+
self.env.pop("_tx_snapshot", None)
|
|
4861
|
+
if self.use_memory_manager and "_tx_memory_snapshot" in self.env:
|
|
4862
|
+
self._managed_objects = dict(self.env["_tx_memory_snapshot"])
|
|
4863
|
+
del self.env["_tx_memory_snapshot"]
|
|
4864
|
+
tx_stack = self.env.get("_tx_stack", [])
|
|
4865
|
+
if tx_stack:
|
|
4866
|
+
tx_stack.pop()
|
|
4867
|
+
self.env["_in_transaction"] = bool(tx_stack)
|
|
4868
|
+
except Exception:
|
|
4869
|
+
# Best-effort revert; never swallow the original exception.
|
|
4870
|
+
pass
|
|
4465
4871
|
raise
|
|
4466
4872
|
|
|
4467
4873
|
if profile_ops and opcode_counts is not None:
|
|
@@ -4568,7 +4974,11 @@ class VM:
|
|
|
4568
4974
|
print(f"[VM TRACE] action error: {result.message}")
|
|
4569
4975
|
return self._unwrap_after_builtin(result)
|
|
4570
4976
|
except Exception:
|
|
4571
|
-
|
|
4977
|
+
# ZAction/ZLambda handling failed — but don't silently swallow
|
|
4978
|
+
# If this was a ZAction, we should NOT fall through to the callable check
|
|
4979
|
+
# because ZAction objects are not directly callable and would be returned raw
|
|
4980
|
+
if ZAction is not None and isinstance(real_fn, (ZAction, ZLambda)):
|
|
4981
|
+
return None
|
|
4572
4982
|
|
|
4573
4983
|
if not callable(real_fn): return real_fn
|
|
4574
4984
|
|
|
@@ -4593,7 +5003,7 @@ class VM:
|
|
|
4593
5003
|
res = await res
|
|
4594
5004
|
return self._unwrap_after_builtin(res)
|
|
4595
5005
|
except Exception as e:
|
|
4596
|
-
return e
|
|
5006
|
+
return ZEvaluationError(f"{type(e).__name__}: {e}")
|
|
4597
5007
|
|
|
4598
5008
|
def _to_coro(self, fn, args):
|
|
4599
5009
|
if asyncio.iscoroutinefunction(fn):
|
|
@@ -4836,6 +5246,56 @@ class VM:
|
|
|
4836
5246
|
def create_vm(mode: str = "auto", use_jit: bool = True, **kwargs) -> VM:
|
|
4837
5247
|
return VM(mode=VMMode(mode.lower()), use_jit=use_jit, **kwargs)
|
|
4838
5248
|
|
|
5249
|
+
|
|
5250
|
+
def _safe_pow(a, b, *, max_exp: int = 10000, max_bits: int = 8192):
|
|
5251
|
+
"""Bound exponentiation to avoid exponent DoS.
|
|
5252
|
+
|
|
5253
|
+
LI11: Prevent pathological cases like `10 ** 10_000_000`.
|
|
5254
|
+
The guard is intentionally conservative; it applies mainly to integer
|
|
5255
|
+
exponentiation.
|
|
5256
|
+
"""
|
|
5257
|
+
try:
|
|
5258
|
+
# Normalize wrapped evaluator objects
|
|
5259
|
+
if hasattr(a, 'value'):
|
|
5260
|
+
a = a.value
|
|
5261
|
+
if hasattr(b, 'value'):
|
|
5262
|
+
b = b.value
|
|
5263
|
+
|
|
5264
|
+
# Only guard integer-ish exponentiation; float pow is already bounded by
|
|
5265
|
+
# IEEE semantics but can still be expensive for huge integer exponents.
|
|
5266
|
+
if isinstance(b, bool):
|
|
5267
|
+
b_int = int(b)
|
|
5268
|
+
elif isinstance(b, int):
|
|
5269
|
+
b_int = b
|
|
5270
|
+
else:
|
|
5271
|
+
# If exponent isn't an int, fall back to Python pow
|
|
5272
|
+
return a ** b
|
|
5273
|
+
|
|
5274
|
+
if abs(b_int) > max_exp:
|
|
5275
|
+
raise OverflowError(f"POW exponent too large: {b_int}")
|
|
5276
|
+
|
|
5277
|
+
# Rough bit-growth guard for integer base
|
|
5278
|
+
if isinstance(a, bool):
|
|
5279
|
+
a_int = int(a)
|
|
5280
|
+
elif isinstance(a, int):
|
|
5281
|
+
a_int = a
|
|
5282
|
+
else:
|
|
5283
|
+
a_int = None
|
|
5284
|
+
|
|
5285
|
+
if a_int is not None and b_int >= 0:
|
|
5286
|
+
# If base is -1, 0, 1 then pow is cheap
|
|
5287
|
+
if a_int not in (-1, 0, 1):
|
|
5288
|
+
est_bits = max(1, abs(a_int).bit_length()) * b_int
|
|
5289
|
+
if est_bits > max_bits:
|
|
5290
|
+
raise OverflowError(
|
|
5291
|
+
f"POW result too large (estimated {est_bits} bits > {max_bits})"
|
|
5292
|
+
)
|
|
5293
|
+
|
|
5294
|
+
return a ** b_int
|
|
5295
|
+
except Exception:
|
|
5296
|
+
# Let callers decide how to surface the error
|
|
5297
|
+
raise
|
|
5298
|
+
|
|
4839
5299
|
def create_high_performance_vm() -> VM:
|
|
4840
5300
|
return create_vm(
|
|
4841
5301
|
mode="auto",
|