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.
Files changed (49) hide show
  1. package/README.md +57 -6
  2. package/package.json +2 -1
  3. package/rust_core/Cargo.lock +603 -0
  4. package/rust_core/Cargo.toml +26 -0
  5. package/rust_core/README.md +15 -0
  6. package/rust_core/pyproject.toml +25 -0
  7. package/rust_core/src/binary_bytecode.rs +543 -0
  8. package/rust_core/src/contract_vm.rs +643 -0
  9. package/rust_core/src/executor.rs +847 -0
  10. package/rust_core/src/hasher.rs +90 -0
  11. package/rust_core/src/lib.rs +71 -0
  12. package/rust_core/src/merkle.rs +128 -0
  13. package/rust_core/src/rust_vm.rs +2313 -0
  14. package/rust_core/src/signature.rs +79 -0
  15. package/rust_core/src/state_adapter.rs +281 -0
  16. package/rust_core/src/validator.rs +116 -0
  17. package/scripts/postinstall.js +34 -2
  18. package/src/zexus/__init__.py +1 -1
  19. package/src/zexus/blockchain/accelerator.py +27 -0
  20. package/src/zexus/blockchain/contract_vm.py +409 -3
  21. package/src/zexus/blockchain/rust_bridge.py +64 -0
  22. package/src/zexus/cli/main.py +1 -1
  23. package/src/zexus/cli/zpm.py +1 -1
  24. package/src/zexus/evaluator/bytecode_compiler.py +150 -52
  25. package/src/zexus/evaluator/core.py +151 -809
  26. package/src/zexus/evaluator/expressions.py +27 -22
  27. package/src/zexus/evaluator/functions.py +171 -126
  28. package/src/zexus/evaluator/statements.py +55 -112
  29. package/src/zexus/module_cache.py +20 -9
  30. package/src/zexus/object.py +330 -38
  31. package/src/zexus/parser/parser.py +69 -14
  32. package/src/zexus/parser/strategy_context.py +228 -5
  33. package/src/zexus/parser/strategy_structural.py +2 -2
  34. package/src/zexus/persistence.py +46 -17
  35. package/src/zexus/security.py +140 -234
  36. package/src/zexus/type_checker.py +44 -5
  37. package/src/zexus/vm/binary_bytecode.py +7 -3
  38. package/src/zexus/vm/bytecode.py +6 -0
  39. package/src/zexus/vm/cache.py +24 -46
  40. package/src/zexus/vm/compiler.py +80 -20
  41. package/src/zexus/vm/fastops.c +1093 -2975
  42. package/src/zexus/vm/gas_metering.py +2 -2
  43. package/src/zexus/vm/memory_pool.py +21 -9
  44. package/src/zexus/vm/vm.py +527 -67
  45. package/src/zexus/zpm/package_manager.py +1 -1
  46. package/src/zexus.egg-info/PKG-INFO +79 -12
  47. package/src/zexus.egg-info/SOURCES.txt +23 -1
  48. package/src/zexus.egg-info/requires.txt +26 -0
  49. package/src/zexus.egg-info/entry_points.txt +0 -4
@@ -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
- try:
1121
- mod = importlib.import_module(module_path)
1122
- key = alias or module_path
1123
- self.env[key] = mod
1124
- self._bump_env_version(key, mod)
1125
- return mod
1126
- except Exception:
1127
- key = alias or module_path
1128
- self.env[key] = None
1129
- self._bump_env_version(key, None)
1130
- return None
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
- try:
1509
- current = sys.getrecursionlimit()
1510
- if current < minimum:
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: Manual environment cleanup for non-managed memory
1572
- # Clear variables that are no longer referenced
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
- initial_count = len(self.env)
1575
- # Keep only builtins and parent env references
1576
- keys_to_remove = []
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
- return None
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
- result = candidate(*args) if callable(candidate) else candidate
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
- env.setdefault("_audit_log", []).append(
2270
- {"timestamp": ts, "action": action, "data": data})
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
- result = candidate(*args) if callable(candidate) else candidate
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 and os.path.exists(path):
3208
- with open(path, 'r') as f:
3209
- stack.append(f.read())
3210
- else:
3211
- stack.append(None)
3212
- except:
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 ** b),
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
- elif op_name == "LOAD_VAR_REG":
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; a = stack.pop() if stack else 0
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 = stack.pop() if stack else 1; a = stack.pop() if stack else 0
3821
- stack.append(a ** b)
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, (list, tuple)) and 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
- if name is None:
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
- with open(path, "w") as f:
4075
- if isinstance(payload, bytes):
4076
- f.write(payload.decode("utf-8"))
4077
- else:
4078
- f.write(str(payload) if payload is not None else "")
4079
- stack.append(True)
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", []).append(entry)
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", []).append(entry)
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
- pass
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",