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
@@ -2092,8 +2092,10 @@ class StatementEvaluatorMixin:
2092
2092
  sandbox_fs = builder.build()
2093
2093
  sandbox_env.set('__sandbox_vfs__', sandbox_fs)
2094
2094
  sandbox_env.set('__sandbox_id__', sandbox_id)
2095
- except Exception:
2096
- pass
2095
+ except Exception as exc:
2096
+ # MEDIUM (M10): If sandbox isolation can't be established, do not
2097
+ # execute the sandbox body under a weaker (or non-existent) policy.
2098
+ return EvaluationError(f"Sandbox initialization failed: {exc}")
2097
2099
 
2098
2100
  # Ensure default sandbox policy exists
2099
2101
  try:
@@ -2228,9 +2230,11 @@ class StatementEvaluatorMixin:
2228
2230
 
2229
2231
  # Set up TX context
2230
2232
  from ..object import Map, String, Integer
2233
+ # SECURITY (H5): Use module-level import instead of __import__
2234
+ import time as _time_mod
2231
2235
  tx_context = Map({
2232
2236
  String("caller"): String("system"), # Default caller
2233
- String("timestamp"): Integer(int(__import__('time').time())),
2237
+ String("timestamp"): Integer(int(_time_mod.time())),
2234
2238
  })
2235
2239
  contract_env.set("TX", tx_context)
2236
2240
 
@@ -3318,7 +3322,17 @@ class StatementEvaluatorMixin:
3318
3322
  # === PERFORMANCE OPTIMIZATION STATEMENTS ===
3319
3323
 
3320
3324
  def eval_native_statement(self, node, env, stack_trace):
3321
- """Evaluate native statement - call C/C++ code directly."""
3325
+ """Evaluate native statement - call C/C++ code directly.
3326
+
3327
+ SECURITY (C3): Requires ZEXUS_ALLOW_NATIVE=1 environment variable.
3328
+ Disabled by default to prevent arbitrary native code loading via ctypes.
3329
+ """
3330
+ import os as _os
3331
+ if _os.environ.get('ZEXUS_ALLOW_NATIVE') != '1':
3332
+ return EvaluationError(
3333
+ "Native statements are disabled for security. "
3334
+ "Set ZEXUS_ALLOW_NATIVE=1 environment variable to enable ctypes FFI."
3335
+ )
3322
3336
  try:
3323
3337
  import ctypes
3324
3338
 
@@ -4190,107 +4204,6 @@ class StatementEvaluatorMixin:
4190
4204
  except Exception as e:
4191
4205
  debug_log("eval_using_statement", f"Error cleaning up resource: {e}")
4192
4206
 
4193
- # === CONCURRENCY & PERFORMANCE STATEMENT EVALUATORS ===
4194
-
4195
- def eval_channel_statement(self, node, env, stack_trace):
4196
- """Evaluate channel statement - declare a message passing channel."""
4197
- from ..concurrency_system import get_concurrency_manager
4198
-
4199
- manager = get_concurrency_manager()
4200
-
4201
- # Get channel name
4202
- channel_name = node.name.value if hasattr(node.name, 'value') else str(node.name)
4203
-
4204
- # Get element type if specified
4205
- element_type = None
4206
- if hasattr(node, 'element_type') and node.element_type:
4207
- element_type = str(node.element_type)
4208
-
4209
- # Get capacity if specified
4210
- capacity = 0
4211
- if hasattr(node, 'capacity') and node.capacity:
4212
- capacity = self.eval_node(node.capacity, env, stack_trace)
4213
- if isinstance(capacity, Integer):
4214
- capacity = capacity.value
4215
- else:
4216
- capacity = 0
4217
-
4218
- # Create channel
4219
- channel = manager.create_channel(channel_name, element_type, capacity)
4220
- debug_log("eval_channel_statement", f"Created channel: {channel_name}")
4221
-
4222
- # Store in environment
4223
- env.set(channel_name, channel)
4224
- return String(f"Channel '{channel_name}' created")
4225
-
4226
- def eval_send_statement(self, node, env, stack_trace):
4227
- """Evaluate send statement - send value to a channel."""
4228
-
4229
- # Evaluate channel expression
4230
- channel = self.eval_node(node.channel_expr, env, stack_trace)
4231
-
4232
- # Evaluate value to send
4233
- value = self.eval_node(node.value_expr, env, stack_trace)
4234
-
4235
- # Send to channel
4236
- if hasattr(channel, 'send'):
4237
- try:
4238
- channel.send(value, timeout=5.0)
4239
- debug_log("eval_send_statement", f"Sent to channel: {value}")
4240
- return String(f"Value sent to channel")
4241
- except Exception as e:
4242
- return String(f"Error sending to channel: {e}")
4243
- else:
4244
- return String(f"Error: not a valid channel")
4245
-
4246
- def eval_receive_statement(self, node, env, stack_trace):
4247
- """Evaluate receive statement - receive value from a channel."""
4248
-
4249
- # Evaluate channel expression
4250
- channel = self.eval_node(node.channel_expr, env, stack_trace)
4251
-
4252
- # Receive from channel
4253
- if hasattr(channel, 'receive'):
4254
- try:
4255
- value = channel.receive(timeout=5.0)
4256
- debug_log("eval_receive_statement", f"Received from channel: {value}")
4257
-
4258
- # Bind to target if specified
4259
- if hasattr(node, 'target') and node.target:
4260
- target_name = node.target.value if hasattr(node.target, 'value') else str(node.target)
4261
- env.set(target_name, value)
4262
-
4263
- return value if value is not None else NULL
4264
- except Exception as e:
4265
- return String(f"Error receiving from channel: {e}")
4266
- else:
4267
- return String(f"Error: not a valid channel")
4268
-
4269
- def eval_atomic_statement(self, node, env, stack_trace):
4270
- """Evaluate atomic statement - execute indivisible operation."""
4271
- from ..concurrency_system import get_concurrency_manager
4272
-
4273
- manager = get_concurrency_manager()
4274
-
4275
- # Create/get atomic region
4276
- atomic_id = f"atomic_{id(node)}"
4277
- atomic = manager.create_atomic(atomic_id)
4278
-
4279
- # Execute atomically
4280
- def execute_block():
4281
- if hasattr(node, 'body') and node.body:
4282
- # Atomic block
4283
- return self.eval_node(node.body, env, stack_trace)
4284
- elif hasattr(node, 'expr') and node.expr:
4285
- # Atomic expression
4286
- return self.eval_node(node.expr, env, stack_trace)
4287
- return NULL
4288
-
4289
- result = atomic.execute(execute_block)
4290
- debug_log("eval_atomic_statement", "Atomic operation completed")
4291
-
4292
- return result if result is not NULL else NULL
4293
-
4294
4207
  # === BLOCKCHAIN STATEMENT EVALUATION ===
4295
4208
 
4296
4209
  def eval_ledger_statement(self, node, env, stack_trace):
@@ -4796,11 +4709,16 @@ class StatementEvaluatorMixin:
4796
4709
  # Store method signatures
4797
4710
  methods = {}
4798
4711
  for method in node.methods:
4799
- method_name = method.name.value if hasattr(method.name, 'value') else str(method.name)
4800
- methods[method_name] = {
4801
- 'params': method.parameters if hasattr(method, 'parameters') else [],
4802
- 'return_type': method.return_type if hasattr(method, 'return_type') else None
4803
- }
4712
+ # methods may be strings (from context parser) or ActionStatement objects
4713
+ if isinstance(method, str):
4714
+ method_name = method
4715
+ methods[method_name] = {'params': [], 'return_type': None}
4716
+ else:
4717
+ method_name = method.name.value if hasattr(method.name, 'value') else str(method.name) if hasattr(method, 'name') else str(method)
4718
+ methods[method_name] = {
4719
+ 'params': method.parameters if hasattr(method, 'parameters') else [],
4720
+ 'return_type': method.return_type if hasattr(method, 'return_type') else None
4721
+ }
4804
4722
 
4805
4723
  # Create protocol object
4806
4724
  protocol_def = {
@@ -4825,6 +4743,22 @@ class StatementEvaluatorMixin:
4825
4743
  # Get variable name
4826
4744
  var_name = node.name.value if hasattr(node.name, 'value') else str(node.name)
4827
4745
 
4746
+ # Ensure persistence backend is initialised on this environment
4747
+ if hasattr(env, '_persistent_storage') and env._persistent_storage is None:
4748
+ try:
4749
+ from ..persistence import PersistentStorage, MemoryTracker
4750
+ # Derive scope from the contract name if available, else use a default
4751
+ scope = env.get("__contract_name__") or "global"
4752
+ if hasattr(scope, 'value'):
4753
+ scope = scope.value
4754
+ scope = str(scope)
4755
+ env._persistent_storage = PersistentStorage(scope)
4756
+ if env._memory_tracker is None:
4757
+ env._memory_tracker = MemoryTracker()
4758
+ env._memory_tracker.start_tracking()
4759
+ except (ImportError, Exception):
4760
+ pass # Persistence module not available — fall through to regular storage
4761
+
4828
4762
  # Evaluate initial value if provided
4829
4763
  value = NULL
4830
4764
  if node.initial_value:
@@ -4835,8 +4769,17 @@ class StatementEvaluatorMixin:
4835
4769
  # Mark as persistent in environment (special marker)
4836
4770
  env.set(f"__persistent_{var_name}__", True)
4837
4771
 
4838
- # Store the actual value
4839
- env.set(var_name, value)
4772
+ # Store via the persistence layer if available, otherwise regular
4773
+ if hasattr(env, 'set_persistent') and hasattr(env, '_persistent_storage') and env._persistent_storage is not None:
4774
+ env.set_persistent(var_name, value)
4775
+ else:
4776
+ env.set(var_name, value)
4777
+
4778
+ # On startup, restore the previously-persisted value (overrides default)
4779
+ if hasattr(env, 'get_persistent') and hasattr(env, '_persistent_storage') and env._persistent_storage is not None:
4780
+ persisted = env.get_persistent(var_name)
4781
+ if persisted is not None:
4782
+ env.store[var_name] = persisted
4840
4783
 
4841
4784
  return NULL
4842
4785
 
@@ -103,6 +103,9 @@ def cache_contract_ast(source_hash: str, ast: Any) -> None:
103
103
  def get_module_candidates(file_path: str, importer_file: str = None) -> list[str]:
104
104
  """Get candidate paths for a module, checking zpm_modules etc.
105
105
 
106
+ SECURITY (H7): All resolved candidates are confined to the project root (cwd).
107
+ Absolute paths and traversals outside the project are rejected.
108
+
106
109
  Args:
107
110
  file_path: The module path to import
108
111
  importer_file: The absolute path of the file doing the importing (for relative imports)
@@ -111,10 +114,16 @@ def get_module_candidates(file_path: str, importer_file: str = None) -> list[str
111
114
  List of candidate absolute paths to check
112
115
  """
113
116
  candidates = []
117
+ project_root = os.path.realpath(os.getcwd())
118
+
119
+ def _is_within_sandbox(resolved: str) -> bool:
120
+ real = os.path.realpath(resolved)
121
+ return real == project_root or real.startswith(project_root + os.sep)
114
122
 
115
123
  if os.path.isabs(file_path):
116
- # Absolute path - use as-is
117
- candidates.append(file_path)
124
+ # SECURITY: Absolute paths are only accepted if within project_root
125
+ if _is_within_sandbox(file_path):
126
+ candidates.append(file_path)
118
127
  else:
119
128
  # Relative path - resolve based on importer's directory
120
129
  if importer_file and file_path.startswith('./'):
@@ -123,7 +132,7 @@ def get_module_candidates(file_path: str, importer_file: str = None) -> list[str
123
132
  resolved_path = os.path.join(importer_dir, file_path[2:]) # Remove './'
124
133
  candidates.append(resolved_path)
125
134
  # Also consider project-root relative paths like "./tests/..."
126
- candidates.append(os.path.join(os.getcwd(), file_path[2:]))
135
+ candidates.append(os.path.join(project_root, file_path[2:]))
127
136
  elif importer_file and file_path.startswith('../'):
128
137
  # Parent directory relative to importing file
129
138
  importer_dir = os.path.dirname(importer_file)
@@ -135,18 +144,20 @@ def get_module_candidates(file_path: str, importer_file: str = None) -> list[str
135
144
  importer_dir = os.path.dirname(importer_file)
136
145
  candidates.append(os.path.join(importer_dir, file_path))
137
146
  # Then relative to current working directory
138
- candidates.append(os.path.join(os.getcwd(), file_path))
147
+ candidates.append(os.path.join(project_root, file_path))
139
148
 
140
149
  # Also check zpm_modules directory
141
- candidates.append(os.path.join(os.getcwd(), 'zpm_modules', file_path))
150
+ candidates.append(os.path.join(project_root, 'zpm_modules', file_path))
142
151
 
143
152
  # Try adding typical extensions (.zx, .zexus)
144
153
  extended_candidates = []
145
154
  for candidate in candidates:
146
- extended_candidates.append(candidate)
147
- if not candidate.endswith(('.zx', '.zexus')):
148
- extended_candidates.append(candidate + '.zx')
149
- extended_candidates.append(candidate + '.zexus')
155
+ # SECURITY: Only keep candidates that resolve within the project root
156
+ if _is_within_sandbox(candidate):
157
+ extended_candidates.append(candidate)
158
+ if not candidate.endswith(('.zx', '.zexus')):
159
+ extended_candidates.append(candidate + '.zx')
160
+ extended_candidates.append(candidate + '.zexus')
150
161
 
151
162
  return extended_candidates
152
163