zexus 1.8.0 → 1.8.2
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 +34 -6
- package/bin/zexus +12 -2
- package/bin/zpics +12 -2
- package/bin/zpm +12 -2
- package/bin/zx +12 -2
- package/bin/zx-deploy +12 -2
- package/bin/zx-dev +12 -2
- package/bin/zx-run +12 -2
- 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 +204 -21
- package/src/zexus/__init__.py +1 -1
- 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 +103 -23
- package/src/zexus/parser/strategy_context.py +318 -6
- 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 +549 -68
- package/src/zexus/vm/memory_pool.py +21 -9
- package/src/zexus/vm/vm.py +609 -95
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +56 -12
- package/src/zexus.egg-info/SOURCES.txt +14 -0
- package/src/zexus.egg-info/entry_points.txt +5 -1
- package/src/zexus.egg-info/requires.txt +26 -0
package/src/zexus/vm/vm.py
CHANGED
|
@@ -31,6 +31,7 @@ from ..object import (
|
|
|
31
31
|
Map as ZMap,
|
|
32
32
|
Null as ZNull,
|
|
33
33
|
EvaluationError as ZEvaluationError,
|
|
34
|
+
DateTime as ZDateTime,
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
# ==================== Backend / Optional Imports ====================
|
|
@@ -436,6 +437,9 @@ class VM:
|
|
|
436
437
|
self._in_execution = 0
|
|
437
438
|
self._native_jit_auto_enabled = False
|
|
438
439
|
self._native_jit_auto_threshold = 700
|
|
440
|
+
# SECURITY (H9): Call-depth tracking to prevent unbounded recursion
|
|
441
|
+
self._call_depth = 0
|
|
442
|
+
self._MAX_CALL_DEPTH = 256
|
|
439
443
|
|
|
440
444
|
# --- Rust VM Adaptive Routing (Phase 3 + Phase 6) ---
|
|
441
445
|
self._rust_vm_available = _RUST_VM_AVAILABLE
|
|
@@ -667,6 +671,22 @@ class VM:
|
|
|
667
671
|
"""Return a child VM to the pool for reuse."""
|
|
668
672
|
if hasattr(self, "_vm_pool") and self._vm_pool is not None:
|
|
669
673
|
if len(self._vm_pool) < 1000:
|
|
674
|
+
# MEDIUM (M14): Scrub references that could leak state across pooled VMs.
|
|
675
|
+
try:
|
|
676
|
+
vm.env = None
|
|
677
|
+
except Exception:
|
|
678
|
+
pass
|
|
679
|
+
for attr in ("_closure_cells", "_name_cache", "_method_cache", "_events", "_tasks"):
|
|
680
|
+
try:
|
|
681
|
+
val = getattr(vm, attr, None)
|
|
682
|
+
if isinstance(val, dict):
|
|
683
|
+
val.clear()
|
|
684
|
+
except Exception:
|
|
685
|
+
pass
|
|
686
|
+
try:
|
|
687
|
+
vm._parent_env = None
|
|
688
|
+
except Exception:
|
|
689
|
+
pass
|
|
670
690
|
self._vm_pool.append(vm)
|
|
671
691
|
|
|
672
692
|
@classmethod
|
|
@@ -763,6 +783,9 @@ class VM:
|
|
|
763
783
|
vm.enable_fast_loop = getattr(parent_vm, "enable_fast_loop", False)
|
|
764
784
|
vm.fast_loop_threshold = getattr(parent_vm, "fast_loop_threshold", 512)
|
|
765
785
|
vm._fast_loop_stats = {"used": False, "reason": ""}
|
|
786
|
+
# SECURITY (H9): Call-depth tracking for child VMs
|
|
787
|
+
vm._call_depth = 0
|
|
788
|
+
vm._MAX_CALL_DEPTH = getattr(parent_vm, "_MAX_CALL_DEPTH", 256)
|
|
766
789
|
|
|
767
790
|
# Settings
|
|
768
791
|
vm.worker_count = parent_vm.worker_count
|
|
@@ -902,6 +925,82 @@ class VM:
|
|
|
902
925
|
self.builtins["__vm_use_module__"] = self._vm_use_module
|
|
903
926
|
if "__vm_from_module__" not in self.builtins:
|
|
904
927
|
self.builtins["__vm_from_module__"] = self._vm_from_module
|
|
928
|
+
# Channel builtins for VM concurrency support
|
|
929
|
+
if "__create_channel__" not in self.builtins:
|
|
930
|
+
self.builtins["__create_channel__"] = self._vm_create_channel
|
|
931
|
+
if "send" not in self.builtins:
|
|
932
|
+
self.builtins["send"] = self._vm_send
|
|
933
|
+
if "receive" not in self.builtins:
|
|
934
|
+
self.builtins["receive"] = self._vm_receive
|
|
935
|
+
if "close_channel" not in self.builtins:
|
|
936
|
+
self.builtins["close_channel"] = self._vm_close_channel
|
|
937
|
+
|
|
938
|
+
# --- Channel builtins for VM ---
|
|
939
|
+
def _vm_create_channel(self, name, element_type=None, capacity=0):
|
|
940
|
+
"""Create a channel and store it in the VM environment."""
|
|
941
|
+
try:
|
|
942
|
+
from ..concurrency_system import Channel
|
|
943
|
+
except ImportError:
|
|
944
|
+
# Fallback: use a simple queue-based channel
|
|
945
|
+
import queue as _queue_mod
|
|
946
|
+
|
|
947
|
+
class _SimpleChannel:
|
|
948
|
+
def __init__(self, name, capacity=0):
|
|
949
|
+
self.name = name
|
|
950
|
+
self._queue = _queue_mod.Queue(maxsize=capacity if capacity else 0)
|
|
951
|
+
self._closed = False
|
|
952
|
+
|
|
953
|
+
def send(self, value, timeout=5.0):
|
|
954
|
+
if self._closed:
|
|
955
|
+
raise RuntimeError(f"Channel '{self.name}' is closed")
|
|
956
|
+
self._queue.put(value, timeout=timeout)
|
|
957
|
+
|
|
958
|
+
def receive(self, timeout=5.0):
|
|
959
|
+
if self._closed and self._queue.empty():
|
|
960
|
+
return None
|
|
961
|
+
try:
|
|
962
|
+
return self._queue.get(timeout=timeout)
|
|
963
|
+
except _queue_mod.Empty:
|
|
964
|
+
return None
|
|
965
|
+
|
|
966
|
+
def close(self):
|
|
967
|
+
self._closed = True
|
|
968
|
+
|
|
969
|
+
Channel = _SimpleChannel
|
|
970
|
+
|
|
971
|
+
cap = int(capacity) if capacity else 0
|
|
972
|
+
ch = Channel(name=name, capacity=cap) if element_type is None else Channel(name=name, element_type=element_type, capacity=cap)
|
|
973
|
+
self.env[name] = ch
|
|
974
|
+
return ch
|
|
975
|
+
|
|
976
|
+
def _vm_send(self, channel, value):
|
|
977
|
+
"""Send value to channel."""
|
|
978
|
+
if channel is None or not hasattr(channel, 'send'):
|
|
979
|
+
return None
|
|
980
|
+
try:
|
|
981
|
+
channel.send(value, timeout=5.0)
|
|
982
|
+
except Exception:
|
|
983
|
+
pass
|
|
984
|
+
return None
|
|
985
|
+
|
|
986
|
+
def _vm_receive(self, channel):
|
|
987
|
+
"""Receive value from channel."""
|
|
988
|
+
if channel is None or not hasattr(channel, 'receive'):
|
|
989
|
+
return None
|
|
990
|
+
try:
|
|
991
|
+
return channel.receive(timeout=5.0)
|
|
992
|
+
except Exception:
|
|
993
|
+
return None
|
|
994
|
+
|
|
995
|
+
def _vm_close_channel(self, channel):
|
|
996
|
+
"""Close a channel."""
|
|
997
|
+
if channel is None or not hasattr(channel, 'close'):
|
|
998
|
+
return None
|
|
999
|
+
try:
|
|
1000
|
+
channel.close()
|
|
1001
|
+
except Exception:
|
|
1002
|
+
pass
|
|
1003
|
+
return None
|
|
905
1004
|
|
|
906
1005
|
def _vm_use_module(self, spec):
|
|
907
1006
|
if spec is None:
|
|
@@ -1147,17 +1246,29 @@ class VM:
|
|
|
1147
1246
|
self._bump_env_version(key, value)
|
|
1148
1247
|
return module_env
|
|
1149
1248
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1249
|
+
# SECURITY (H6): Only allow safe Python stdlib modules via importlib.
|
|
1250
|
+
# Dangerous modules (os, subprocess, socket, ctypes, etc.) are blocked.
|
|
1251
|
+
_SAFE_PYTHON_MODULES = frozenset({
|
|
1252
|
+
'math', 'json', 'hashlib', 'base64', 'collections', 'datetime',
|
|
1253
|
+
'decimal', 'fractions', 'itertools', 'functools', 'operator',
|
|
1254
|
+
'string', 'textwrap', 'unicodedata', 'enum', 'dataclasses',
|
|
1255
|
+
'copy', 'pprint', 'reprlib', 'numbers', 'cmath', 'statistics',
|
|
1256
|
+
'random', 'secrets', 'uuid', 're', 'struct', 'binascii',
|
|
1257
|
+
'html', 'urllib.parse', 'ipaddress', 'typing', 'abc',
|
|
1258
|
+
})
|
|
1259
|
+
if module_path in _SAFE_PYTHON_MODULES:
|
|
1260
|
+
try:
|
|
1261
|
+
mod = importlib.import_module(module_path)
|
|
1262
|
+
key = alias or module_path
|
|
1263
|
+
self.env[key] = mod
|
|
1264
|
+
self._bump_env_version(key, mod)
|
|
1265
|
+
return mod
|
|
1266
|
+
except Exception:
|
|
1267
|
+
pass
|
|
1268
|
+
key = alias or module_path
|
|
1269
|
+
self.env[key] = None
|
|
1270
|
+
self._bump_env_version(key, None)
|
|
1271
|
+
return None
|
|
1161
1272
|
|
|
1162
1273
|
def _get_cached_method(self, target: Any, method_name: str):
|
|
1163
1274
|
if target is None:
|
|
@@ -1255,7 +1366,23 @@ class VM:
|
|
|
1255
1366
|
pct = (count / total_ops * 100) if total_ops else 0.0
|
|
1256
1367
|
print(f"[VM PROFILE] {op_name} count={count} pct={pct:.2f}%")
|
|
1257
1368
|
return result
|
|
1258
|
-
|
|
1369
|
+
except Exception:
|
|
1370
|
+
# LOGIC (L4): If an exception escapes a TX block, revert state to snapshot.
|
|
1371
|
+
if self.env.get("_in_transaction", False):
|
|
1372
|
+
try:
|
|
1373
|
+
self.env["_blockchain_state"] = dict(self.env.get("_tx_snapshot", {}))
|
|
1374
|
+
self.env["_tx_pending_state"] = {}
|
|
1375
|
+
self.env.pop("_tx_snapshot", None)
|
|
1376
|
+
if self.use_memory_manager and "_tx_memory_snapshot" in self.env:
|
|
1377
|
+
self._managed_objects = dict(self.env["_tx_memory_snapshot"])
|
|
1378
|
+
del self.env["_tx_memory_snapshot"]
|
|
1379
|
+
tx_stack = self.env.get("_tx_stack", [])
|
|
1380
|
+
if tx_stack:
|
|
1381
|
+
tx_stack.pop()
|
|
1382
|
+
self.env["_in_transaction"] = bool(tx_stack)
|
|
1383
|
+
except Exception:
|
|
1384
|
+
pass
|
|
1385
|
+
raise
|
|
1259
1386
|
finally:
|
|
1260
1387
|
self._in_execution = max(0, getattr(self, "_in_execution", 1) - 1)
|
|
1261
1388
|
self._total_execution_time += (time.perf_counter() - start_time)
|
|
@@ -1535,12 +1662,9 @@ class VM:
|
|
|
1535
1662
|
return {'jit_enabled': False}
|
|
1536
1663
|
|
|
1537
1664
|
def _ensure_recursion_headroom(self, minimum: int = 5000):
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
sys.setrecursionlimit(minimum)
|
|
1542
|
-
except Exception:
|
|
1543
|
-
pass
|
|
1665
|
+
# SECURITY (H13): No longer increases sys.setrecursionlimit.
|
|
1666
|
+
# VM-level call depth is enforced by _call_depth / _MAX_CALL_DEPTH instead.
|
|
1667
|
+
pass
|
|
1544
1668
|
|
|
1545
1669
|
def clear_jit_cache(self):
|
|
1546
1670
|
if self.use_jit and self.jit_compiler:
|
|
@@ -1598,25 +1722,13 @@ class VM:
|
|
|
1598
1722
|
del self._managed_objects[name]
|
|
1599
1723
|
return {'collected': collected, 'gc_time': gc_time}
|
|
1600
1724
|
|
|
1601
|
-
# Fallback:
|
|
1602
|
-
#
|
|
1725
|
+
# Fallback: Without a memory manager, do NOT delete user variables.
|
|
1726
|
+
# Deleting non-underscore env keys breaks program state and can silently
|
|
1727
|
+
# corrupt execution. Keep this as a no-op to preserve semantics.
|
|
1603
1728
|
if force:
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
for key in list(self.env.keys()):
|
|
1608
|
-
# Don't remove special keys or builtins
|
|
1609
|
-
if not key.startswith('_') and key not in self.builtins:
|
|
1610
|
-
keys_to_remove.append(key)
|
|
1611
|
-
|
|
1612
|
-
# Remove temporary variables
|
|
1613
|
-
for key in keys_to_remove:
|
|
1614
|
-
del self.env[key]
|
|
1615
|
-
|
|
1616
|
-
cleared = initial_count - len(self.env)
|
|
1617
|
-
return {'collected': cleared, 'message': 'Environment variables cleared'}
|
|
1618
|
-
|
|
1619
|
-
return {'collected': 0, 'message': 'Memory manager disabled or not forced'}
|
|
1729
|
+
return {'collected': 0, 'message': 'No-op: memory manager disabled (force ignored)'}
|
|
1730
|
+
|
|
1731
|
+
return {'collected': 0, 'message': 'Memory manager disabled'}
|
|
1620
1732
|
|
|
1621
1733
|
|
|
1622
1734
|
def _allocate_managed(self, value: Any, name: str = None, root: bool = False) -> int:
|
|
@@ -1904,8 +2016,10 @@ class VM:
|
|
|
1904
2016
|
stack_append = stack.append
|
|
1905
2017
|
# stack_pop = stack.pop
|
|
1906
2018
|
def stack_pop():
|
|
2019
|
+
# MEDIUM (M15): Match the main VM stack behaviour; stack underflow
|
|
2020
|
+
# should not silently return None.
|
|
1907
2021
|
if not stack:
|
|
1908
|
-
|
|
2022
|
+
raise IndexError("pop from empty stack")
|
|
1909
2023
|
return stack.pop()
|
|
1910
2024
|
|
|
1911
2025
|
ip = 0
|
|
@@ -1993,15 +2107,32 @@ class VM:
|
|
|
1993
2107
|
a = stack_pop() if stack else 0
|
|
1994
2108
|
if hasattr(a, 'value'): a = a.value
|
|
1995
2109
|
if hasattr(b, 'value'): b = b.value
|
|
1996
|
-
|
|
2110
|
+
# Type coercion: if either operand is a string, convert both to string
|
|
2111
|
+
if isinstance(a, str) or isinstance(b, str):
|
|
2112
|
+
stack_append(str(a if a is not None else '') + str(b if b is not None else ''))
|
|
2113
|
+
else:
|
|
2114
|
+
if a is None: a = 0
|
|
2115
|
+
if b is None: b = 0
|
|
2116
|
+
stack_append(a + b)
|
|
1997
2117
|
elif op_name == "SUB":
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
if
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2118
|
+
b_raw = stack_pop() if stack else 0
|
|
2119
|
+
a_raw = stack_pop() if stack else 0
|
|
2120
|
+
# DateTime arithmetic
|
|
2121
|
+
if isinstance(a_raw, ZDateTime) and isinstance(b_raw, ZDateTime):
|
|
2122
|
+
stack_append(a_raw.timestamp - b_raw.timestamp)
|
|
2123
|
+
elif isinstance(a_raw, ZDateTime):
|
|
2124
|
+
b = b_raw.value if hasattr(b_raw, 'value') else b_raw
|
|
2125
|
+
if b is None: b = 0
|
|
2126
|
+
stack_append(ZDateTime(a_raw.timestamp - float(b)))
|
|
2127
|
+
else:
|
|
2128
|
+
a = a_raw.value if hasattr(a_raw, 'value') else a_raw
|
|
2129
|
+
b = b_raw.value if hasattr(b_raw, 'value') else b_raw
|
|
2130
|
+
if a is None: a = 0
|
|
2131
|
+
if b is None: b = 0
|
|
2132
|
+
try:
|
|
2133
|
+
stack_append(a - b)
|
|
2134
|
+
except TypeError:
|
|
2135
|
+
stack_append(0)
|
|
2005
2136
|
elif op_name == "MUL":
|
|
2006
2137
|
b = stack_pop() if stack else 0
|
|
2007
2138
|
a = stack_pop() if stack else 0
|
|
@@ -2017,7 +2148,19 @@ class VM:
|
|
|
2017
2148
|
elif op_name == "MOD":
|
|
2018
2149
|
b = stack_pop() if stack else 1
|
|
2019
2150
|
a = stack_pop() if stack else 0
|
|
2151
|
+
if hasattr(a, 'value'): a = a.value
|
|
2152
|
+
if hasattr(b, 'value'): b = b.value
|
|
2153
|
+
if a is None: a = 0
|
|
2154
|
+
if b is None: b = 1
|
|
2020
2155
|
stack_append(a % b if b != 0 else 0)
|
|
2156
|
+
elif op_name == "POW":
|
|
2157
|
+
b = stack_pop() if stack else 1
|
|
2158
|
+
a = stack_pop() if stack else 0
|
|
2159
|
+
if hasattr(a, 'value'): a = a.value
|
|
2160
|
+
if hasattr(b, 'value'): b = b.value
|
|
2161
|
+
if a is None: a = 0
|
|
2162
|
+
if b is None: b = 0
|
|
2163
|
+
stack_append(_safe_pow(a, b))
|
|
2021
2164
|
elif op_name == "EQ":
|
|
2022
2165
|
b = stack_pop() if stack else None
|
|
2023
2166
|
a = stack_pop() if stack else None
|
|
@@ -2049,6 +2192,18 @@ class VM:
|
|
|
2049
2192
|
elif op_name == "NOT":
|
|
2050
2193
|
a = stack_pop() if stack else False
|
|
2051
2194
|
stack_append(not a)
|
|
2195
|
+
elif op_name == "AND":
|
|
2196
|
+
b = stack_pop() if stack else False
|
|
2197
|
+
a = stack_pop() if stack else False
|
|
2198
|
+
if hasattr(a, 'value'): a = a.value
|
|
2199
|
+
if hasattr(b, 'value'): b = b.value
|
|
2200
|
+
stack_append(bool(a) and bool(b))
|
|
2201
|
+
elif op_name == "OR":
|
|
2202
|
+
b = stack_pop() if stack else False
|
|
2203
|
+
a = stack_pop() if stack else False
|
|
2204
|
+
if hasattr(a, 'value'): a = a.value
|
|
2205
|
+
if hasattr(b, 'value'): b = b.value
|
|
2206
|
+
stack_append(bool(a) or bool(b))
|
|
2052
2207
|
elif op_name == "NEG":
|
|
2053
2208
|
a = stack_pop() if stack else 0
|
|
2054
2209
|
stack_append(-a)
|
|
@@ -2058,6 +2213,52 @@ class VM:
|
|
|
2058
2213
|
cond = stack_pop() if stack else None
|
|
2059
2214
|
if not cond:
|
|
2060
2215
|
ip = operand
|
|
2216
|
+
elif op_name == "JUMP_IF_TRUE":
|
|
2217
|
+
cond = stack_pop() if stack else None
|
|
2218
|
+
if hasattr(cond, 'value'):
|
|
2219
|
+
cond = cond.value
|
|
2220
|
+
if cond:
|
|
2221
|
+
ip = operand
|
|
2222
|
+
elif op_name == "IMPORT":
|
|
2223
|
+
# Mirror async IMPORT behavior (minimal)
|
|
2224
|
+
try:
|
|
2225
|
+
if isinstance(operand, (list, tuple)) and operand:
|
|
2226
|
+
mod_name = const(operand[0])
|
|
2227
|
+
alias = const(operand[1]) if len(operand) > 1 else ""
|
|
2228
|
+
names = const(operand[2]) if len(operand) > 2 else []
|
|
2229
|
+
is_named = const(operand[3]) if len(operand) > 3 else False
|
|
2230
|
+
else:
|
|
2231
|
+
mod_name = const(operand)
|
|
2232
|
+
alias, names, is_named = "", [], False
|
|
2233
|
+
self._execute_import(mod_name, alias=alias or "", names=names, is_named=bool(is_named))
|
|
2234
|
+
except Exception:
|
|
2235
|
+
pass
|
|
2236
|
+
elif op_name == "EXPORT":
|
|
2237
|
+
# vm/compiler.py emits: LOAD_NAME <name_idx>; EXPORT <name_idx>
|
|
2238
|
+
try:
|
|
2239
|
+
if isinstance(operand, int):
|
|
2240
|
+
name = const(operand)
|
|
2241
|
+
value = stack_pop() if stack else None
|
|
2242
|
+
elif isinstance(operand, (list, tuple)) and operand:
|
|
2243
|
+
name = const(operand[0])
|
|
2244
|
+
value = stack_pop() if stack else None
|
|
2245
|
+
if len(operand) > 1:
|
|
2246
|
+
value = const(operand[1])
|
|
2247
|
+
else:
|
|
2248
|
+
name = stack_pop() if stack else None
|
|
2249
|
+
value = stack_pop() if stack else None
|
|
2250
|
+
|
|
2251
|
+
export_fn = getattr(self.env, "export", None)
|
|
2252
|
+
if callable(export_fn):
|
|
2253
|
+
try:
|
|
2254
|
+
export_fn(name, value)
|
|
2255
|
+
except Exception:
|
|
2256
|
+
pass
|
|
2257
|
+
else:
|
|
2258
|
+
self.env[name] = value
|
|
2259
|
+
self._bump_env_version(name, value)
|
|
2260
|
+
except Exception:
|
|
2261
|
+
pass
|
|
2061
2262
|
elif op_name == "RETURN":
|
|
2062
2263
|
return stack_pop() if stack else None
|
|
2063
2264
|
elif op_name == "BUILD_LIST":
|
|
@@ -2203,7 +2404,17 @@ class VM:
|
|
|
2203
2404
|
result = attr(*args)
|
|
2204
2405
|
elif isinstance(target, dict) and method_name in target:
|
|
2205
2406
|
candidate = target[method_name]
|
|
2206
|
-
|
|
2407
|
+
# Handle Builtin objects (have .fn), ZAction/ZLambda (have .call),
|
|
2408
|
+
# and regular callables
|
|
2409
|
+
if callable(candidate):
|
|
2410
|
+
result = candidate(*args)
|
|
2411
|
+
elif hasattr(candidate, 'fn') and callable(candidate.fn):
|
|
2412
|
+
# Builtin from evaluator — call inner fn
|
|
2413
|
+
result = candidate.fn(*args)
|
|
2414
|
+
elif hasattr(candidate, 'call') and callable(candidate.call):
|
|
2415
|
+
result = candidate.call(*args)
|
|
2416
|
+
else:
|
|
2417
|
+
result = candidate
|
|
2207
2418
|
else:
|
|
2208
2419
|
result = attr
|
|
2209
2420
|
except Exception:
|
|
@@ -2394,6 +2605,7 @@ class VM:
|
|
|
2394
2605
|
elif op_name == "LEDGER_APPEND":
|
|
2395
2606
|
entry = stack_pop()
|
|
2396
2607
|
ledger = env.setdefault("_ledger", [])
|
|
2608
|
+
# MEDIUM (M12): Keep ledger bounded to prevent memory exhaustion.
|
|
2397
2609
|
if len(ledger) < 10000: # Size limit
|
|
2398
2610
|
if isinstance(entry, dict) and "timestamp" not in entry:
|
|
2399
2611
|
entry["timestamp"] = time.time()
|
|
@@ -2429,8 +2641,16 @@ class VM:
|
|
|
2429
2641
|
data = stack_pop(); action = stack_pop()
|
|
2430
2642
|
if hasattr(action, 'value'): action = action.value
|
|
2431
2643
|
if hasattr(data, 'value'): data = data.value
|
|
2432
|
-
|
|
2433
|
-
|
|
2644
|
+
# MEDIUM (M13): Cap audit log growth to prevent memory exhaustion.
|
|
2645
|
+
audit = env.setdefault("_audit_log", [])
|
|
2646
|
+
entry = {"timestamp": ts, "action": action, "data": data}
|
|
2647
|
+
if len(audit) >= 10000:
|
|
2648
|
+
# Drop oldest entries; keep most recent.
|
|
2649
|
+
try:
|
|
2650
|
+
del audit[:1000]
|
|
2651
|
+
except Exception:
|
|
2652
|
+
audit[:] = audit[-9000:]
|
|
2653
|
+
audit.append(entry)
|
|
2434
2654
|
|
|
2435
2655
|
elif op_name == "RESTRICT_ACCESS":
|
|
2436
2656
|
restriction = stack_pop(); prop = stack_pop(); obj = stack_pop()
|
|
@@ -2455,6 +2675,48 @@ class VM:
|
|
|
2455
2675
|
if caller and caller not in allowed:
|
|
2456
2676
|
raise ZEvaluationError(f"Access denied: '{r_key}' restricted to allowed addresses")
|
|
2457
2677
|
|
|
2678
|
+
elif op_name == "SPAWN":
|
|
2679
|
+
# Sync SPAWN: schedule the function to run in a background thread
|
|
2680
|
+
import threading
|
|
2681
|
+
val = stack_pop() if stack else None
|
|
2682
|
+
if callable(val):
|
|
2683
|
+
# val is a callable — run it in a daemon thread
|
|
2684
|
+
t = threading.Thread(target=val, daemon=True)
|
|
2685
|
+
t.start()
|
|
2686
|
+
stack_append(t)
|
|
2687
|
+
elif isinstance(val, dict) and "bytecode" in val:
|
|
2688
|
+
# val is a function descriptor — execute its bytecode in a thread
|
|
2689
|
+
func_desc = val
|
|
2690
|
+
def _run_func_desc(fd=func_desc):
|
|
2691
|
+
try:
|
|
2692
|
+
child_vm = VM.create_child(parent_vm=self, env=dict(env))
|
|
2693
|
+
child_vm._run_stack_bytecode_sync(fd["bytecode"], debug=False)
|
|
2694
|
+
self._return_vm_to_pool(child_vm)
|
|
2695
|
+
except Exception:
|
|
2696
|
+
pass
|
|
2697
|
+
t = threading.Thread(target=_run_func_desc, daemon=True)
|
|
2698
|
+
t.start()
|
|
2699
|
+
stack_append(t)
|
|
2700
|
+
else:
|
|
2701
|
+
# Already a result (not spawnable) — just push it back
|
|
2702
|
+
stack_append(val)
|
|
2703
|
+
|
|
2704
|
+
elif op_name == "AWAIT":
|
|
2705
|
+
import threading
|
|
2706
|
+
val = stack_pop() if stack else None
|
|
2707
|
+
if isinstance(val, threading.Thread):
|
|
2708
|
+
val.join(timeout=30)
|
|
2709
|
+
stack_append(None)
|
|
2710
|
+
elif isinstance(val, str) and val in self._tasks:
|
|
2711
|
+
# asyncio task — run in event loop
|
|
2712
|
+
try:
|
|
2713
|
+
result = self._run_coroutine_sync(self._tasks[val])
|
|
2714
|
+
stack_append(result)
|
|
2715
|
+
except Exception:
|
|
2716
|
+
stack_append(None)
|
|
2717
|
+
else:
|
|
2718
|
+
stack_append(val)
|
|
2719
|
+
|
|
2458
2720
|
else:
|
|
2459
2721
|
# Truly unknown op — fallback to async path
|
|
2460
2722
|
return self._run_coroutine_sync(self._run_stack_bytecode(bytecode, debug))
|
|
@@ -2569,6 +2831,22 @@ class VM:
|
|
|
2569
2831
|
"""Synchronous callable invocation for fast dispatch."""
|
|
2570
2832
|
if fn is None:
|
|
2571
2833
|
return None
|
|
2834
|
+
# SECURITY (H9): Enforce call-depth limit
|
|
2835
|
+
call_depth = getattr(self, '_call_depth', 0)
|
|
2836
|
+
max_depth = getattr(self, '_MAX_CALL_DEPTH', 256)
|
|
2837
|
+
if call_depth >= max_depth:
|
|
2838
|
+
raise RecursionError(
|
|
2839
|
+
f"VM call depth exceeded maximum ({max_depth}). "
|
|
2840
|
+
"Possible infinite recursion."
|
|
2841
|
+
)
|
|
2842
|
+
self._call_depth = call_depth + 1
|
|
2843
|
+
try:
|
|
2844
|
+
return self._invoke_callable_sync_inner(fn, args)
|
|
2845
|
+
finally:
|
|
2846
|
+
self._call_depth = getattr(self, '_call_depth', 1) - 1
|
|
2847
|
+
|
|
2848
|
+
def _invoke_callable_sync_inner(self, fn, args):
|
|
2849
|
+
"""Inner implementation of callable invocation."""
|
|
2572
2850
|
real_fn = fn.fn if hasattr(fn, "fn") else fn
|
|
2573
2851
|
ZAction, ZLambda = _get_action_types()
|
|
2574
2852
|
if ZAction is not None and isinstance(real_fn, (ZAction, ZLambda)):
|
|
@@ -2773,14 +3051,20 @@ class VM:
|
|
|
2773
3051
|
_cached_ZAction, _cached_ZLambda = _get_action_types()
|
|
2774
3052
|
|
|
2775
3053
|
class _EvalStack:
|
|
2776
|
-
__slots__ = ("data", "sp")
|
|
3054
|
+
__slots__ = ("data", "sp", "_max_depth")
|
|
3055
|
+
_MAX_STACK_DEPTH = 100_000 # SECURITY (H8): prevent unbounded stack growth
|
|
2777
3056
|
|
|
2778
3057
|
def __init__(self, capacity: int):
|
|
2779
3058
|
base = max(32, capacity)
|
|
2780
3059
|
self.data = [None] * base
|
|
2781
3060
|
self.sp = 0
|
|
3061
|
+
self._max_depth = _EvalStack._MAX_STACK_DEPTH
|
|
2782
3062
|
|
|
2783
3063
|
def _ensure_capacity(self):
|
|
3064
|
+
if self.sp >= self._max_depth:
|
|
3065
|
+
raise OverflowError(
|
|
3066
|
+
f"VM stack overflow: depth {self.sp} exceeds maximum {self._max_depth}"
|
|
3067
|
+
)
|
|
2784
3068
|
if self.sp >= len(self.data):
|
|
2785
3069
|
self.data.extend([None] * len(self.data))
|
|
2786
3070
|
|
|
@@ -2931,22 +3215,57 @@ class VM:
|
|
|
2931
3215
|
|
|
2932
3216
|
def _binary_op(func):
|
|
2933
3217
|
def wrapper(_):
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3218
|
+
b_raw = stack.pop() if stack else 0
|
|
3219
|
+
a_raw = stack.pop() if stack else 0
|
|
3220
|
+
# Keep raw objects for DateTime-aware arithmetic
|
|
3221
|
+
a = _unwrap(a_raw)
|
|
3222
|
+
b = _unwrap(b_raw)
|
|
2938
3223
|
if isinstance(a, ZEvaluationError):
|
|
2939
3224
|
stack.append(a)
|
|
2940
3225
|
return
|
|
2941
3226
|
if isinstance(b, ZEvaluationError):
|
|
2942
3227
|
stack.append(b)
|
|
2943
3228
|
return
|
|
3229
|
+
# DateTime arithmetic
|
|
3230
|
+
if isinstance(a_raw, ZDateTime) or isinstance(b_raw, ZDateTime):
|
|
3231
|
+
a_dt = a_raw if isinstance(a_raw, ZDateTime) else None
|
|
3232
|
+
b_dt = b_raw if isinstance(b_raw, ZDateTime) else None
|
|
3233
|
+
if a_dt and b_dt:
|
|
3234
|
+
# DateTime - DateTime = seconds difference
|
|
3235
|
+
stack.append(a_dt.timestamp - b_dt.timestamp)
|
|
3236
|
+
elif a_dt:
|
|
3237
|
+
# DateTime +/- number
|
|
3238
|
+
val = float(b) if b is not None else 0.0
|
|
3239
|
+
try:
|
|
3240
|
+
result = func(a_dt.timestamp, val)
|
|
3241
|
+
stack.append(ZDateTime(result))
|
|
3242
|
+
except Exception:
|
|
3243
|
+
stack.append(0)
|
|
3244
|
+
else:
|
|
3245
|
+
val = float(a) if a is not None else 0.0
|
|
3246
|
+
try:
|
|
3247
|
+
result = func(val, b_dt.timestamp)
|
|
3248
|
+
stack.append(result)
|
|
3249
|
+
except Exception:
|
|
3250
|
+
stack.append(0)
|
|
3251
|
+
return
|
|
3252
|
+
if a is None: a = 0
|
|
3253
|
+
if b is None: b = 0
|
|
3254
|
+
# Type coercion for string concatenation in ADD
|
|
3255
|
+
if isinstance(a, str) or isinstance(b, str):
|
|
3256
|
+
try:
|
|
3257
|
+
stack.append(func(a, b))
|
|
3258
|
+
except TypeError:
|
|
3259
|
+
stack.append(str(a) + str(b))
|
|
3260
|
+
return
|
|
2944
3261
|
try:
|
|
2945
3262
|
stack.append(func(a, b))
|
|
2946
|
-
except
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3263
|
+
except TypeError:
|
|
3264
|
+
# Graceful fallback — coerce to strings
|
|
3265
|
+
try:
|
|
3266
|
+
stack.append(str(a) + str(b))
|
|
3267
|
+
except Exception:
|
|
3268
|
+
stack.append(0)
|
|
2950
3269
|
return wrapper
|
|
2951
3270
|
|
|
2952
3271
|
def _binary_bool_op(func):
|
|
@@ -3166,7 +3485,16 @@ class VM:
|
|
|
3166
3485
|
result = attr(*args)
|
|
3167
3486
|
elif isinstance(target, dict) and method_name in target:
|
|
3168
3487
|
candidate = target[method_name]
|
|
3169
|
-
|
|
3488
|
+
# Handle Builtin objects (have .fn), ZAction/ZLambda (have .call),
|
|
3489
|
+
# and regular callables
|
|
3490
|
+
if callable(candidate):
|
|
3491
|
+
result = candidate(*args)
|
|
3492
|
+
elif hasattr(candidate, 'fn') and callable(candidate.fn):
|
|
3493
|
+
result = candidate.fn(*args)
|
|
3494
|
+
elif hasattr(candidate, 'call') and callable(candidate.call):
|
|
3495
|
+
result = candidate.call(*args)
|
|
3496
|
+
else:
|
|
3497
|
+
result = candidate
|
|
3170
3498
|
else:
|
|
3171
3499
|
result = attr
|
|
3172
3500
|
if _verbose_active and self._call_method_trace_count <= 25:
|
|
@@ -3242,19 +3570,41 @@ class VM:
|
|
|
3242
3570
|
stack.append(-a)
|
|
3243
3571
|
|
|
3244
3572
|
def _op_add(_):
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3573
|
+
b_raw = stack_pop() if stack else 0
|
|
3574
|
+
a_raw = stack_pop() if stack else 0
|
|
3575
|
+
# DateTime arithmetic
|
|
3576
|
+
if isinstance(a_raw, ZDateTime) or isinstance(b_raw, ZDateTime):
|
|
3577
|
+
a_dt = a_raw if isinstance(a_raw, ZDateTime) else None
|
|
3578
|
+
b_dt = b_raw if isinstance(b_raw, ZDateTime) else None
|
|
3579
|
+
if a_dt and b_dt:
|
|
3580
|
+
stack_append(a_dt.timestamp + b_dt.timestamp)
|
|
3581
|
+
elif a_dt:
|
|
3582
|
+
b = _unwrap(b_raw)
|
|
3583
|
+
if b is None: b = 0
|
|
3584
|
+
stack_append(ZDateTime(a_dt.timestamp + float(b)))
|
|
3585
|
+
else:
|
|
3586
|
+
a = _unwrap(a_raw)
|
|
3587
|
+
if a is None: a = 0
|
|
3588
|
+
stack_append(ZDateTime(b_raw.timestamp + float(a)))
|
|
3589
|
+
return
|
|
3590
|
+
b = _unwrap(b_raw)
|
|
3591
|
+
a = _unwrap(a_raw)
|
|
3251
3592
|
if isinstance(a, ZEvaluationError):
|
|
3252
3593
|
stack_append(a)
|
|
3253
3594
|
return
|
|
3254
3595
|
if isinstance(b, ZEvaluationError):
|
|
3255
3596
|
stack_append(b)
|
|
3256
3597
|
return
|
|
3257
|
-
|
|
3598
|
+
# Type coercion: if either operand is a string, convert both to string
|
|
3599
|
+
if isinstance(a, str) or isinstance(b, str):
|
|
3600
|
+
stack_append(str(a if a is not None else '') + str(b if b is not None else ''))
|
|
3601
|
+
else:
|
|
3602
|
+
if a is None: a = 0
|
|
3603
|
+
if b is None: b = 0
|
|
3604
|
+
try:
|
|
3605
|
+
stack_append(a + b)
|
|
3606
|
+
except TypeError:
|
|
3607
|
+
stack_append(str(a) + str(b))
|
|
3258
3608
|
|
|
3259
3609
|
def _op_not(_):
|
|
3260
3610
|
a = stack.pop() if stack else False
|
|
@@ -3366,13 +3716,33 @@ class VM:
|
|
|
3366
3716
|
def _op_read(_):
|
|
3367
3717
|
path = stack.pop() if stack else None
|
|
3368
3718
|
try:
|
|
3369
|
-
import os
|
|
3370
|
-
if path
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3719
|
+
import os as _os
|
|
3720
|
+
if path:
|
|
3721
|
+
# SECURITY (C11): Sandbox file reads to the module root (env __DIR__) when present.
|
|
3722
|
+
# This keeps sandboxing while allowing tests/runtime to set the project root without
|
|
3723
|
+
# relying on global process CWD.
|
|
3724
|
+
sandbox = _os.getcwd()
|
|
3725
|
+
try:
|
|
3726
|
+
env_dir = env_get("__DIR__", None)
|
|
3727
|
+
if env_dir:
|
|
3728
|
+
sandbox = _os.path.realpath(str(env_dir))
|
|
3729
|
+
except Exception:
|
|
3730
|
+
pass
|
|
3731
|
+
|
|
3732
|
+
if isinstance(path, str) and '\x00' not in path:
|
|
3733
|
+
candidate = path
|
|
3734
|
+
if _os.path.isabs(candidate):
|
|
3735
|
+
resolved = _os.path.realpath(candidate)
|
|
3736
|
+
else:
|
|
3737
|
+
resolved = _os.path.realpath(_os.path.join(sandbox, candidate))
|
|
3738
|
+
|
|
3739
|
+
if resolved == sandbox or resolved.startswith(sandbox + _os.sep):
|
|
3740
|
+
if _os.path.exists(resolved):
|
|
3741
|
+
with open(resolved, 'r') as f:
|
|
3742
|
+
stack.append(f.read())
|
|
3743
|
+
return
|
|
3744
|
+
stack.append(None)
|
|
3745
|
+
except (IOError, OSError, PermissionError):
|
|
3376
3746
|
stack.append(None)
|
|
3377
3747
|
|
|
3378
3748
|
def _op_store_func(operand):
|
|
@@ -3399,7 +3769,7 @@ class VM:
|
|
|
3399
3769
|
"MUL": _binary_op(lambda a, b: a * b),
|
|
3400
3770
|
"DIV": _binary_op(lambda a, b: a / b if b != 0 else 0),
|
|
3401
3771
|
"MOD": _binary_op(lambda a, b: a % b if b != 0 else 0),
|
|
3402
|
-
"POW": _binary_op(lambda a, b: a
|
|
3772
|
+
"POW": _binary_op(lambda a, b: _safe_pow(a, b)),
|
|
3403
3773
|
"NEG": _op_neg,
|
|
3404
3774
|
"EQ": _binary_bool_op(lambda a, b: a == b),
|
|
3405
3775
|
"NEQ": _binary_bool_op(lambda a, b: a != b),
|
|
@@ -3456,6 +3826,7 @@ class VM:
|
|
|
3456
3826
|
(self.enable_fast_loop or auto_fast_loop)
|
|
3457
3827
|
and not profile_ops
|
|
3458
3828
|
and not gas_enabled # Never skip gas metering (including gas_light)
|
|
3829
|
+
and not gas_light # SECURITY (C13): also block fast loop under gas_light
|
|
3459
3830
|
and not trace_interval
|
|
3460
3831
|
and trace_ip_range is None
|
|
3461
3832
|
and not trace_loads_active
|
|
@@ -3472,6 +3843,8 @@ class VM:
|
|
|
3472
3843
|
reasons.append("opcode_profile")
|
|
3473
3844
|
if gas_enabled and not gas_light:
|
|
3474
3845
|
reasons.append("gas")
|
|
3846
|
+
if gas_light:
|
|
3847
|
+
reasons.append("gas_light")
|
|
3475
3848
|
if trace_interval:
|
|
3476
3849
|
reasons.append("trace_interval")
|
|
3477
3850
|
if trace_ip_range is not None:
|
|
@@ -3665,7 +4038,7 @@ class VM:
|
|
|
3665
4038
|
self._jit_registers = {}
|
|
3666
4039
|
self._jit_registers[reg] = value
|
|
3667
4040
|
|
|
3668
|
-
|
|
4041
|
+
res = _safe_pow(v1, v2)
|
|
3669
4042
|
reg, name_idx = operand
|
|
3670
4043
|
name = const(name_idx)
|
|
3671
4044
|
value = _resolve(name)
|
|
@@ -3960,12 +4333,31 @@ class VM:
|
|
|
3960
4333
|
# Auto-unwrap evaluator objects
|
|
3961
4334
|
if hasattr(a, 'value'): a = a.value
|
|
3962
4335
|
if hasattr(b, 'value'): b = b.value
|
|
3963
|
-
|
|
4336
|
+
# Type coercion: if either operand is a string, convert both to string
|
|
4337
|
+
if isinstance(a, str) or isinstance(b, str):
|
|
4338
|
+
stack.append(str(a if a is not None else '') + str(b if b is not None else ''))
|
|
4339
|
+
else:
|
|
4340
|
+
if a is None: a = 0
|
|
4341
|
+
if b is None: b = 0
|
|
4342
|
+
stack.append(a + b)
|
|
3964
4343
|
elif op_name == "SUB":
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
if
|
|
3968
|
-
|
|
4344
|
+
b_raw = stack.pop() if stack else 0; a_raw = stack.pop() if stack else 0
|
|
4345
|
+
# DateTime arithmetic
|
|
4346
|
+
if isinstance(a_raw, ZDateTime) and isinstance(b_raw, ZDateTime):
|
|
4347
|
+
stack.append(a_raw.timestamp - b_raw.timestamp)
|
|
4348
|
+
elif isinstance(a_raw, ZDateTime):
|
|
4349
|
+
b = b_raw.value if hasattr(b_raw, 'value') else b_raw
|
|
4350
|
+
if b is None: b = 0
|
|
4351
|
+
stack.append(ZDateTime(a_raw.timestamp - float(b)))
|
|
4352
|
+
else:
|
|
4353
|
+
a = a_raw.value if hasattr(a_raw, 'value') else a_raw
|
|
4354
|
+
b = b_raw.value if hasattr(b_raw, 'value') else b_raw
|
|
4355
|
+
if a is None: a = 0
|
|
4356
|
+
if b is None: b = 0
|
|
4357
|
+
try:
|
|
4358
|
+
stack.append(a - b)
|
|
4359
|
+
except TypeError:
|
|
4360
|
+
stack.append(0)
|
|
3969
4361
|
elif op_name == "MUL":
|
|
3970
4362
|
b = stack.pop() if stack else 0; a = stack.pop() if stack else 0
|
|
3971
4363
|
if hasattr(a, 'value'): a = a.value
|
|
@@ -3977,11 +4369,17 @@ class VM:
|
|
|
3977
4369
|
if hasattr(b, 'value'): b = b.value
|
|
3978
4370
|
stack.append(a / b if b != 0 else 0)
|
|
3979
4371
|
elif op_name == "MOD":
|
|
3980
|
-
b = stack.pop() if stack else 1
|
|
4372
|
+
b = _unwrap(stack.pop() if stack else 1)
|
|
4373
|
+
a = _unwrap(stack.pop() if stack else 0)
|
|
4374
|
+
if a is None: a = 0
|
|
4375
|
+
if b is None: b = 1
|
|
3981
4376
|
stack.append(a % b if b != 0 else 0)
|
|
3982
4377
|
elif op_name == "POW":
|
|
3983
|
-
b =
|
|
3984
|
-
stack.
|
|
4378
|
+
b = _unwrap(stack.pop() if stack else 0)
|
|
4379
|
+
a = _unwrap(stack.pop() if stack else 0)
|
|
4380
|
+
if a is None: a = 0
|
|
4381
|
+
if b is None: b = 0
|
|
4382
|
+
stack.append(_safe_pow(a, b))
|
|
3985
4383
|
elif op_name == "NEG":
|
|
3986
4384
|
a = stack.pop() if stack else 0
|
|
3987
4385
|
stack.append(-a)
|
|
@@ -4006,6 +4404,14 @@ class VM:
|
|
|
4006
4404
|
elif op_name == "NOT":
|
|
4007
4405
|
a = stack.pop() if stack else False
|
|
4008
4406
|
stack.append(not a)
|
|
4407
|
+
elif op_name == "AND":
|
|
4408
|
+
b = _unwrap(stack.pop() if stack else False)
|
|
4409
|
+
a = _unwrap(stack.pop() if stack else False)
|
|
4410
|
+
stack.append(bool(a) and bool(b))
|
|
4411
|
+
elif op_name == "OR":
|
|
4412
|
+
b = _unwrap(stack.pop() if stack else False)
|
|
4413
|
+
a = _unwrap(stack.pop() if stack else False)
|
|
4414
|
+
stack.append(bool(a) or bool(b))
|
|
4009
4415
|
|
|
4010
4416
|
# --- Control Flow ---
|
|
4011
4417
|
elif op_name == "JUMP":
|
|
@@ -4013,6 +4419,9 @@ class VM:
|
|
|
4013
4419
|
elif op_name == "JUMP_IF_FALSE":
|
|
4014
4420
|
cond = stack.pop() if stack else None
|
|
4015
4421
|
if not cond: ip = operand
|
|
4422
|
+
elif op_name == "JUMP_IF_TRUE":
|
|
4423
|
+
cond = _unwrap(stack.pop() if stack else None)
|
|
4424
|
+
if cond: ip = operand
|
|
4016
4425
|
elif op_name == "RETURN":
|
|
4017
4426
|
return stack.pop() if stack else None
|
|
4018
4427
|
|
|
@@ -4208,15 +4617,20 @@ class VM:
|
|
|
4208
4617
|
self._execute_import(mod_name, alias=alias or "", names=names, is_named=bool(is_named))
|
|
4209
4618
|
|
|
4210
4619
|
elif op_name == "EXPORT":
|
|
4620
|
+
# vm/compiler.py emits: LOAD_NAME <name_idx>; EXPORT <name_idx>
|
|
4621
|
+
# Support both legacy tuple operand and int operand.
|
|
4211
4622
|
name = None
|
|
4212
4623
|
value = None
|
|
4213
|
-
if isinstance(operand,
|
|
4624
|
+
if isinstance(operand, int):
|
|
4625
|
+
name = const(operand)
|
|
4626
|
+
value = stack.pop() if stack else None
|
|
4627
|
+
elif isinstance(operand, (list, tuple)) and operand:
|
|
4214
4628
|
name = const(operand[0])
|
|
4629
|
+
value = stack.pop() if stack else None
|
|
4215
4630
|
if len(operand) > 1:
|
|
4216
4631
|
value = const(operand[1])
|
|
4217
|
-
|
|
4632
|
+
else:
|
|
4218
4633
|
name = stack.pop() if stack else None
|
|
4219
|
-
if value is None:
|
|
4220
4634
|
value = stack.pop() if stack else None
|
|
4221
4635
|
|
|
4222
4636
|
export_fn = getattr(self.env, "export", None)
|
|
@@ -4234,12 +4648,34 @@ class VM:
|
|
|
4234
4648
|
path = stack.pop() if stack else None
|
|
4235
4649
|
try:
|
|
4236
4650
|
if path is not None:
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4651
|
+
# SECURITY (C12): Sandbox file writes to the module root (env __DIR__) when present.
|
|
4652
|
+
import os as _os_w
|
|
4653
|
+
sandbox = _os_w.getcwd()
|
|
4654
|
+
try:
|
|
4655
|
+
env_dir = self.env.get("__DIR__", None)
|
|
4656
|
+
if env_dir:
|
|
4657
|
+
sandbox = _os_w.path.realpath(str(env_dir))
|
|
4658
|
+
except Exception:
|
|
4659
|
+
pass
|
|
4660
|
+
|
|
4661
|
+
path_str = str(path)
|
|
4662
|
+
if "\x00" in path_str:
|
|
4663
|
+
stack.append(False)
|
|
4664
|
+
continue
|
|
4665
|
+
|
|
4666
|
+
if _os_w.path.isabs(path_str):
|
|
4667
|
+
resolved = _os_w.path.realpath(path_str)
|
|
4668
|
+
else:
|
|
4669
|
+
resolved = _os_w.path.realpath(_os_w.path.join(sandbox, path_str))
|
|
4670
|
+
if not (resolved == sandbox or resolved.startswith(sandbox + _os_w.sep)):
|
|
4671
|
+
stack.append(False) # path traversal denied
|
|
4672
|
+
else:
|
|
4673
|
+
with open(resolved, "w") as f:
|
|
4674
|
+
if isinstance(payload, bytes):
|
|
4675
|
+
f.write(payload.decode("utf-8"))
|
|
4676
|
+
else:
|
|
4677
|
+
f.write(str(payload) if payload is not None else "")
|
|
4678
|
+
stack.append(True)
|
|
4243
4679
|
else:
|
|
4244
4680
|
stack.append(False)
|
|
4245
4681
|
except Exception:
|
|
@@ -4561,7 +4997,13 @@ class VM:
|
|
|
4561
4997
|
data = data.value if hasattr(data, 'value') else data
|
|
4562
4998
|
|
|
4563
4999
|
entry = {"timestamp": ts, "action": action, "data": data}
|
|
4564
|
-
self.env.setdefault("_audit_log", [])
|
|
5000
|
+
audit = self.env.setdefault("_audit_log", [])
|
|
5001
|
+
if len(audit) >= 10000:
|
|
5002
|
+
try:
|
|
5003
|
+
del audit[:1000]
|
|
5004
|
+
except Exception:
|
|
5005
|
+
audit[:] = audit[-9000:]
|
|
5006
|
+
audit.append(entry)
|
|
4565
5007
|
if self.debug: print(f"[AUDIT] {entry}")
|
|
4566
5008
|
|
|
4567
5009
|
elif op_name == "RESTRICT_ACCESS":
|
|
@@ -4606,7 +5048,9 @@ class VM:
|
|
|
4606
5048
|
entry = stack.pop() if stack else None
|
|
4607
5049
|
if isinstance(entry, dict) and "timestamp" not in entry:
|
|
4608
5050
|
entry["timestamp"] = time.time()
|
|
4609
|
-
self.env.setdefault("_ledger", [])
|
|
5051
|
+
ledger = self.env.setdefault("_ledger", [])
|
|
5052
|
+
if len(ledger) < 10000:
|
|
5053
|
+
ledger.append(entry)
|
|
4610
5054
|
|
|
4611
5055
|
elif op_name in ("PARALLEL_START", "PARALLEL_END"):
|
|
4612
5056
|
# Marker ops for parallel execution - no-op in stack VM
|
|
@@ -4625,6 +5069,22 @@ class VM:
|
|
|
4625
5069
|
if debug: print(f"[VM ERROR RECOVERY] {e}")
|
|
4626
5070
|
self.env.setdefault("_errors", []).append(str(e))
|
|
4627
5071
|
else:
|
|
5072
|
+
# LOGIC (L4): If an exception escapes a TX block, revert state to snapshot.
|
|
5073
|
+
if self.env.get("_in_transaction", False):
|
|
5074
|
+
try:
|
|
5075
|
+
self.env["_blockchain_state"] = dict(self.env.get("_tx_snapshot", {}))
|
|
5076
|
+
self.env["_tx_pending_state"] = {}
|
|
5077
|
+
self.env.pop("_tx_snapshot", None)
|
|
5078
|
+
if self.use_memory_manager and "_tx_memory_snapshot" in self.env:
|
|
5079
|
+
self._managed_objects = dict(self.env["_tx_memory_snapshot"])
|
|
5080
|
+
del self.env["_tx_memory_snapshot"]
|
|
5081
|
+
tx_stack = self.env.get("_tx_stack", [])
|
|
5082
|
+
if tx_stack:
|
|
5083
|
+
tx_stack.pop()
|
|
5084
|
+
self.env["_in_transaction"] = bool(tx_stack)
|
|
5085
|
+
except Exception:
|
|
5086
|
+
# Best-effort revert; never swallow the original exception.
|
|
5087
|
+
pass
|
|
4628
5088
|
raise
|
|
4629
5089
|
|
|
4630
5090
|
if profile_ops and opcode_counts is not None:
|
|
@@ -4731,7 +5191,11 @@ class VM:
|
|
|
4731
5191
|
print(f"[VM TRACE] action error: {result.message}")
|
|
4732
5192
|
return self._unwrap_after_builtin(result)
|
|
4733
5193
|
except Exception:
|
|
4734
|
-
|
|
5194
|
+
# ZAction/ZLambda handling failed — but don't silently swallow
|
|
5195
|
+
# If this was a ZAction, we should NOT fall through to the callable check
|
|
5196
|
+
# because ZAction objects are not directly callable and would be returned raw
|
|
5197
|
+
if ZAction is not None and isinstance(real_fn, (ZAction, ZLambda)):
|
|
5198
|
+
return None
|
|
4735
5199
|
|
|
4736
5200
|
if not callable(real_fn): return real_fn
|
|
4737
5201
|
|
|
@@ -4756,7 +5220,7 @@ class VM:
|
|
|
4756
5220
|
res = await res
|
|
4757
5221
|
return self._unwrap_after_builtin(res)
|
|
4758
5222
|
except Exception as e:
|
|
4759
|
-
return e
|
|
5223
|
+
return ZEvaluationError(f"{type(e).__name__}: {e}")
|
|
4760
5224
|
|
|
4761
5225
|
def _to_coro(self, fn, args):
|
|
4762
5226
|
if asyncio.iscoroutinefunction(fn):
|
|
@@ -4999,6 +5463,56 @@ class VM:
|
|
|
4999
5463
|
def create_vm(mode: str = "auto", use_jit: bool = True, **kwargs) -> VM:
|
|
5000
5464
|
return VM(mode=VMMode(mode.lower()), use_jit=use_jit, **kwargs)
|
|
5001
5465
|
|
|
5466
|
+
|
|
5467
|
+
def _safe_pow(a, b, *, max_exp: int = 10000, max_bits: int = 8192):
|
|
5468
|
+
"""Bound exponentiation to avoid exponent DoS.
|
|
5469
|
+
|
|
5470
|
+
LI11: Prevent pathological cases like `10 ** 10_000_000`.
|
|
5471
|
+
The guard is intentionally conservative; it applies mainly to integer
|
|
5472
|
+
exponentiation.
|
|
5473
|
+
"""
|
|
5474
|
+
try:
|
|
5475
|
+
# Normalize wrapped evaluator objects
|
|
5476
|
+
if hasattr(a, 'value'):
|
|
5477
|
+
a = a.value
|
|
5478
|
+
if hasattr(b, 'value'):
|
|
5479
|
+
b = b.value
|
|
5480
|
+
|
|
5481
|
+
# Only guard integer-ish exponentiation; float pow is already bounded by
|
|
5482
|
+
# IEEE semantics but can still be expensive for huge integer exponents.
|
|
5483
|
+
if isinstance(b, bool):
|
|
5484
|
+
b_int = int(b)
|
|
5485
|
+
elif isinstance(b, int):
|
|
5486
|
+
b_int = b
|
|
5487
|
+
else:
|
|
5488
|
+
# If exponent isn't an int, fall back to Python pow
|
|
5489
|
+
return a ** b
|
|
5490
|
+
|
|
5491
|
+
if abs(b_int) > max_exp:
|
|
5492
|
+
raise OverflowError(f"POW exponent too large: {b_int}")
|
|
5493
|
+
|
|
5494
|
+
# Rough bit-growth guard for integer base
|
|
5495
|
+
if isinstance(a, bool):
|
|
5496
|
+
a_int = int(a)
|
|
5497
|
+
elif isinstance(a, int):
|
|
5498
|
+
a_int = a
|
|
5499
|
+
else:
|
|
5500
|
+
a_int = None
|
|
5501
|
+
|
|
5502
|
+
if a_int is not None and b_int >= 0:
|
|
5503
|
+
# If base is -1, 0, 1 then pow is cheap
|
|
5504
|
+
if a_int not in (-1, 0, 1):
|
|
5505
|
+
est_bits = max(1, abs(a_int).bit_length()) * b_int
|
|
5506
|
+
if est_bits > max_bits:
|
|
5507
|
+
raise OverflowError(
|
|
5508
|
+
f"POW result too large (estimated {est_bits} bits > {max_bits})"
|
|
5509
|
+
)
|
|
5510
|
+
|
|
5511
|
+
return a ** b_int
|
|
5512
|
+
except Exception:
|
|
5513
|
+
# Let callers decide how to surface the error
|
|
5514
|
+
raise
|
|
5515
|
+
|
|
5002
5516
|
def create_high_performance_vm() -> VM:
|
|
5003
5517
|
return create_vm(
|
|
5004
5518
|
mode="auto",
|