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
@@ -1,5 +1,6 @@
1
1
  # src/zexus/evaluator/expressions.py
2
2
  import os
3
+ import traceback as _tb
3
4
 
4
5
  from ..zexus_ast import (
5
6
  IntegerLiteral, FloatLiteral, StringLiteral, ListLiteral, MapLiteral,
@@ -139,7 +140,6 @@ class ExpressionEvaluatorMixin:
139
140
  if hasattr(env, 'store'):
140
141
  env_keys = list(env.store.keys())
141
142
  # Use direct print to ensure visibility during debugging
142
- import traceback as _tb
143
143
  stack_snip = ''.join(_tb.format_stack(limit=5)[-3:])
144
144
  # print(f"[DEBUG] Identifier not found: {node.value}; env_keys={env_keys}\nStack snippet:\n{stack_snip}")
145
145
  except Exception:
@@ -367,13 +367,27 @@ class ExpressionEvaluatorMixin:
367
367
  return self.eval_string_infix(operator, left, right)
368
368
 
369
369
  # String repetition: "x" * 100 or 100 * "x"
370
+ # SECURITY (H3): Cap repetition to prevent memory exhaustion
370
371
  elif operator == "*":
372
+ _MAX_STRING_REPEAT = 1_000_000 # 1 MB max
371
373
  if isinstance(left, String) and isinstance(right, Integer):
372
- # "x" * 100
373
- return String(left.value * right.value)
374
+ n = right.value
375
+ if n < 0:
376
+ n = 0
377
+ if n > _MAX_STRING_REPEAT:
378
+ return EvaluationError(
379
+ f"String repetition count {n} exceeds maximum ({_MAX_STRING_REPEAT})"
380
+ )
381
+ return String(left.value * n)
374
382
  elif isinstance(left, Integer) and isinstance(right, String):
375
- # 100 * "x"
376
- return String(right.value * left.value)
383
+ n = left.value
384
+ if n < 0:
385
+ n = 0
386
+ if n > _MAX_STRING_REPEAT:
387
+ return EvaluationError(
388
+ f"String repetition count {n} exceeds maximum ({_MAX_STRING_REPEAT})"
389
+ )
390
+ return String(right.value * n)
377
391
 
378
392
  # Array Concatenation
379
393
  elif operator == "+" and isinstance(left, List) and isinstance(right, List):
@@ -844,23 +858,14 @@ class ExpressionEvaluatorMixin:
844
858
  error_msg += f"\n Promise created at: {awaitable.stack_trace}"
845
859
  return EvaluationError(error_msg)
846
860
  else:
847
- # Promise is still pending - this shouldn't happen with current implementation
848
- # but we can spin-wait briefly
849
- import time
850
- max_wait = 1.0 # 1 second timeout
851
- waited = 0.0
852
- while not awaitable.is_resolved() and waited < max_wait:
853
- time.sleep(0.001) # 1ms
854
- waited += 0.001
855
-
856
- if awaitable.is_resolved():
857
- try:
858
- result = awaitable.get_value()
859
- return result if result is not None else NULL
860
- except Exception as e:
861
- return EvaluationError(f"Promise rejected: {e}")
862
- else:
863
- return EvaluationError("Await timeout: promise did not resolve")
861
+ # LI6: Avoid busy-waiting (sleep(0.001)) which burns CPU and
862
+ # makes await non-deterministic. In the current interpreter,
863
+ # unresolved promises are not awaited synchronously.
864
+ return EvaluationError(
865
+ "Awaited promise is still pending. "
866
+ "This runtime does not support blocking awaits; "
867
+ "ensure the promise resolves before awaiting it."
868
+ )
864
869
 
865
870
  # Await a Coroutine
866
871
  elif obj_type == "COROUTINE":
@@ -565,6 +565,8 @@ class FunctionEvaluatorMixin:
565
565
  found = any(elem.value == target.value for elem in obj.elements
566
566
  if hasattr(elem, 'value') and hasattr(target, 'value'))
567
567
  return TRUE if found else FALSE
568
+ elif method_name == "is_empty":
569
+ return TRUE if len(obj.elements) == 0 else FALSE
568
570
 
569
571
  # === Coroutine Methods ===
570
572
  from ..object import Coroutine
@@ -593,11 +595,23 @@ class FunctionEvaluatorMixin:
593
595
 
594
596
  if method_name == "has":
595
597
  key = args[0].value if hasattr(args[0], 'value') else str(args[0])
596
- return TRUE if key in obj.pairs else FALSE
598
+ # Try plain key first, then try String-wrapped key for normalization
599
+ if key in obj.pairs:
600
+ return TRUE
601
+ str_key = String(key) if isinstance(key, str) else key
602
+ if str_key in obj.pairs:
603
+ return TRUE
604
+ return FALSE
597
605
  elif method_name == "get":
598
606
  key = args[0].value if hasattr(args[0], 'value') else str(args[0])
599
607
  default = args[1] if len(args) > 1 else NULL
600
- return obj.pairs.get(key, default)
608
+ # Try plain key first, then String-wrapped key for normalization
609
+ if key in obj.pairs:
610
+ return obj.pairs[key]
611
+ str_key = String(key) if isinstance(key, str) else key
612
+ if str_key in obj.pairs:
613
+ return obj.pairs[str_key]
614
+ return default
601
615
  elif method_name == "keys":
602
616
  # Return array of all keys
603
617
  return List([String(k) if isinstance(k, str) else k for k in obj.pairs.keys()])
@@ -2634,103 +2648,26 @@ class FunctionEvaluatorMixin:
2634
2648
  return EvaluationError(f"eval_file() zexus execution error: {str(e)}")
2635
2649
 
2636
2650
  elif language == "py" or language == "python":
2637
- # Execute Python code
2638
- try:
2639
- exec_globals = {}
2640
- exec(content, exec_globals)
2641
- # Return the result if there's a 'result' variable
2642
- if 'result' in exec_globals:
2643
- result_val = exec_globals['result']
2644
- # Convert Python types to Zexus types
2645
- if isinstance(result_val, str):
2646
- return String(result_val)
2647
- elif isinstance(result_val, int):
2648
- return Integer(result_val)
2649
- elif isinstance(result_val, float):
2650
- return Float(result_val)
2651
- elif isinstance(result_val, bool):
2652
- return Boolean(result_val)
2653
- elif isinstance(result_val, list):
2654
- return List([Integer(x) if isinstance(x, int) else String(str(x)) for x in result_val])
2655
- return NULL
2656
- except Exception as e:
2657
- return EvaluationError(f"eval_file() python execution error: {str(e)}")
2651
+ # SECURITY (C1): exec() disabled — arbitrary Python execution is unsafe
2652
+ return EvaluationError(
2653
+ "eval_file() for Python is disabled for security reasons. "
2654
+ "Use Zexus native code or the FFI bridge instead."
2655
+ )
2658
2656
 
2659
2657
  elif language in ["cpp", "c++", "c", "rs", "rust", "go"]:
2660
2658
  # For compiled languages, try to compile and run
2661
2659
  return EvaluationError(f"eval_file() for {language} requires compilation - not yet implemented")
2662
2660
 
2663
2661
  elif language == "js" or language == "javascript":
2664
- # Execute JavaScript (if Node.js is available)
2665
- try:
2666
- result = subprocess.run(['node', '-e', content],
2667
- capture_output=True,
2668
- text=True,
2669
- timeout=5)
2670
- if result.returncode != 0:
2671
- return EvaluationError(f"JavaScript error: {result.stderr}")
2672
- return String(result.stdout.strip())
2673
- except FileNotFoundError:
2674
- return EvaluationError("Node.js not found - cannot execute JavaScript")
2675
- except Exception as e:
2676
- return EvaluationError(f"eval_file() js execution error: {str(e)}")
2662
+ # SECURITY (C2): subprocess.run(['node',...]) disabled — arbitrary JS execution is unsafe
2663
+ return EvaluationError(
2664
+ "eval_file() for JavaScript is disabled for security reasons. "
2665
+ "Use Zexus native code instead."
2666
+ )
2677
2667
 
2678
2668
  else:
2679
2669
  return EvaluationError(f"Unsupported language: {language}")
2680
2670
 
2681
- # Contract Assertions
2682
- def _require(*a):
2683
- """Assert a condition in smart contracts: require(condition, message)
2684
-
2685
- Throws an error if condition is false. Essential for contract validation.
2686
-
2687
- Example:
2688
- require(balance >= amount, "Insufficient balance")
2689
- require(sender == owner, "Not authorized")
2690
- require(value > 0, "Amount must be positive")
2691
- """
2692
- if len(a) < 1 or len(a) > 2:
2693
- return EvaluationError("require() takes 1-2 arguments: require(condition, [message])")
2694
-
2695
- condition = a[0]
2696
- message = a[1].value if len(a) > 1 and isinstance(a[1], String) else "Requirement failed"
2697
-
2698
- # Check if condition is truthy
2699
- from .utils import is_truthy
2700
- if not is_truthy(condition):
2701
- # Return error with contract-specific formatting
2702
- return EvaluationError(f"Contract requirement failed: {message}")
2703
-
2704
- # Condition passed, return NULL
2705
- return NULL
2706
-
2707
- # Contract Assertions
2708
- def _require(*a):
2709
- """Assert a condition in smart contracts: require(condition, message)
2710
-
2711
- Throws an error if condition is false. Essential for contract validation.
2712
- Note: This is a fallback for contexts where the require statement isn't available.
2713
-
2714
- Example:
2715
- require(balance >= amount, "Insufficient balance")
2716
- require(sender == owner, "Not authorized")
2717
- require(value > 0, "Amount must be positive")
2718
- """
2719
- if len(a) < 1 or len(a) > 2:
2720
- return EvaluationError("require() takes 1-2 arguments: require(condition, [message])")
2721
-
2722
- condition = a[0]
2723
- message = a[1].value if len(a) > 1 and isinstance(a[1], String) else "Requirement failed"
2724
-
2725
- # Check if condition is truthy
2726
- from .utils import is_truthy
2727
- if not is_truthy(condition):
2728
- # Return error with contract-specific formatting
2729
- return EvaluationError(f"Contract requirement failed: {message}")
2730
-
2731
- # Condition passed, return NULL
2732
- return NULL
2733
-
2734
2671
  # Map/Object helper functions
2735
2672
  def _keys(*a):
2736
2673
  """Get all keys from a map: keys(map) -> [key1, key2, ...]"""
@@ -2783,8 +2720,6 @@ class FunctionEvaluatorMixin:
2783
2720
  "to_hex": Builtin(_to_hex, "to_hex"),
2784
2721
  "from_hex": Builtin(_from_hex, "from_hex"),
2785
2722
  "sqrt": Builtin(_sqrt, "sqrt"),
2786
- "require": Builtin(_require, "require"),
2787
- "require": Builtin(_require, "require"),
2788
2723
  "input": Builtin(_input, "input"),
2789
2724
  "hash_password": Builtin(_hash_password, "hash_password"),
2790
2725
  "verify_password": Builtin(_verify_password, "verify_password"),
@@ -2832,10 +2767,8 @@ class FunctionEvaluatorMixin:
2832
2767
  "uppercase": Builtin(_uppercase, "uppercase"),
2833
2768
  "lowercase": Builtin(_lowercase, "lowercase"),
2834
2769
  "split": Builtin(_split, "split"),
2835
- "random": Builtin(_random, "random"),
2836
2770
  "persist_set": Builtin(_persist_set, "persist_set"),
2837
2771
  "persist_get": Builtin(_persist_get, "persist_get"),
2838
- "input": Builtin(_input, "input"),
2839
2772
  "len": Builtin(_len, "len"),
2840
2773
  "type": Builtin(_type, "type"),
2841
2774
  "first": Builtin(_first, "first"),
@@ -3204,7 +3137,6 @@ class FunctionEvaluatorMixin:
3204
3137
  "receive": Builtin(_receive, "receive"),
3205
3138
  "close_channel": Builtin(_close_channel, "close_channel"),
3206
3139
  "async": Builtin(_async, "async"),
3207
- "sleep": Builtin(_sleep, "sleep"),
3208
3140
  "spawn": Builtin(_spawn, "spawn"),
3209
3141
  "wait_group": Builtin(_wait_group, "wait_group"),
3210
3142
  "wg_add": Builtin(_wg_add, "wg_add"),
@@ -3622,8 +3554,10 @@ class FunctionEvaluatorMixin:
3622
3554
 
3623
3555
  def _memory_stats(*a):
3624
3556
  """Get memory tracking statistics: memory_stats()"""
3625
- import sys
3626
3557
  import gc
3558
+ import os
3559
+ import sys
3560
+ import tracemalloc
3627
3561
 
3628
3562
  # Get process memory usage
3629
3563
  try:
@@ -3633,12 +3567,24 @@ class FunctionEvaluatorMixin:
3633
3567
  current_bytes = mem_info.rss # Resident Set Size
3634
3568
  peak_bytes = getattr(mem_info, 'peak_wset', mem_info.rss) # Windows has peak_wset
3635
3569
  except (ImportError, AttributeError):
3636
- # Fallback: use Python's internal memory tracking
3637
- current_bytes = sys.getsizeof(gc.get_objects())
3638
- peak_bytes = current_bytes
3570
+ # LI8: Avoid gc.get_objects() (very slow). Prefer OS ru_maxrss or tracemalloc.
3571
+ current_bytes = 0
3572
+ peak_bytes = 0
3573
+ try:
3574
+ import resource
3575
+ ru = resource.getrusage(resource.RUSAGE_SELF)
3576
+ # Linux: KB, macOS: bytes
3577
+ ru_maxrss = getattr(ru, 'ru_maxrss', 0) or 0
3578
+ if sys.platform == 'darwin':
3579
+ peak_bytes = int(ru_maxrss)
3580
+ else:
3581
+ peak_bytes = int(ru_maxrss) * 1024
3582
+ current_bytes = peak_bytes
3583
+ except Exception:
3584
+ if tracemalloc.is_tracing():
3585
+ current_bytes, peak_bytes = tracemalloc.get_traced_memory()
3639
3586
 
3640
- # Get GC statistics
3641
- gc_count = len(gc.get_objects())
3587
+ # Get GC statistics (fast)
3642
3588
  gc_collections = sum(gc.get_count())
3643
3589
 
3644
3590
  # Get environment-specific tracking if available
@@ -3652,7 +3598,7 @@ class FunctionEvaluatorMixin:
3652
3598
  String("current"): Integer(current_bytes),
3653
3599
  String("peak"): Integer(peak_bytes),
3654
3600
  String("gc_count"): Integer(gc_collections),
3655
- String("objects"): Integer(gc_count),
3601
+ String("objects"): Integer(-1),
3656
3602
  String("tracked_objects"): Integer(tracked_objects)
3657
3603
  })
3658
3604
 
@@ -3829,6 +3775,94 @@ class FunctionEvaluatorMixin:
3829
3775
  "clear_mocks": Builtin(_clear_mocks, "clear_mocks"),
3830
3776
  "set_execution_mode": Builtin(_set_execution_mode, "set_execution_mode"),
3831
3777
  })
3778
+
3779
+ # ----- INT-005 through INT-009: missing directive builtins -----
3780
+ self._register_missing_directive_builtins()
3781
+
3782
+ def _register_missing_directive_builtins(self):
3783
+ """Register track_memory, cache, throttle, audit, verify builtins (INT-005..INT-009)."""
3784
+
3785
+ def _track_memory(*a):
3786
+ """Enable memory tracking: track_memory() or track_memory(options_map)"""
3787
+ env = getattr(self, '_current_env', None)
3788
+ if env and hasattr(env, 'enable_memory_tracking'):
3789
+ env.enable_memory_tracking()
3790
+ return String("Memory tracking enabled")
3791
+ return String("Memory tracking enabled (no-op — persistence module not loaded)")
3792
+
3793
+ def _cache(*a):
3794
+ """Declare a named cache: cache(name, options_map)
3795
+ Options: {ttl: seconds}
3796
+ Returns the cache handle (currently a lightweight Map stub).
3797
+ """
3798
+ name = str(a[0].value) if a and hasattr(a[0], 'value') else "default"
3799
+ ttl = 300 # default 5 min
3800
+ if len(a) >= 2 and hasattr(a[1], 'pairs'):
3801
+ for k, v in a[1].pairs.items():
3802
+ key_str = k.value if hasattr(k, 'value') else str(k)
3803
+ if key_str == "ttl" and hasattr(v, 'value'):
3804
+ ttl = int(v.value)
3805
+ # Store cache metadata in environment
3806
+ env = getattr(self, '_current_env', None)
3807
+ if env:
3808
+ env.set(f"__cache_{name}_ttl__", Integer(ttl))
3809
+ return Map({String("name"): String(name), String("ttl"): Integer(ttl)})
3810
+
3811
+ def _throttle(*a):
3812
+ """Set up rate-limiting: throttle(name, options_map)
3813
+ Options: {requests_per_minute: N}
3814
+ """
3815
+ name = str(a[0].value) if a and hasattr(a[0], 'value') else "default"
3816
+ rpm = 60
3817
+ if len(a) >= 2 and hasattr(a[1], 'pairs'):
3818
+ for k, v in a[1].pairs.items():
3819
+ key_str = k.value if hasattr(k, 'value') else str(k)
3820
+ if key_str == "requests_per_minute" and hasattr(v, 'value'):
3821
+ rpm = int(v.value)
3822
+ env = getattr(self, '_current_env', None)
3823
+ if env:
3824
+ env.set(f"__throttle_{name}_rpm__", Integer(rpm))
3825
+ return Map({String("name"): String(name), String("requests_per_minute"): Integer(rpm)})
3826
+
3827
+ def _audit(*a):
3828
+ """Log an audit event: audit(event_name, data_map)"""
3829
+ event_name = str(a[0].value) if a and hasattr(a[0], 'value') else "unknown"
3830
+ data = a[1] if len(a) >= 2 else NULL
3831
+ # Store in environment audit trail
3832
+ env = getattr(self, '_current_env', None)
3833
+ if env:
3834
+ trail = env.get("__audit_trail__")
3835
+ if trail is None or not isinstance(trail, List):
3836
+ trail = List([])
3837
+ env.set("__audit_trail__", trail)
3838
+ trail.elements.append(Map({String("event"): String(event_name), String("data"): data}))
3839
+ return String(f"Audit logged: {event_name}")
3840
+
3841
+ def _verify(*a):
3842
+ """Alias for require(): verify(condition, message)
3843
+ Throws if condition is falsy.
3844
+ """
3845
+ if len(a) < 1:
3846
+ return EvaluationError("verify() requires at least 1 argument: condition")
3847
+ condition = a[0]
3848
+ msg = str(a[1].value) if len(a) >= 2 and hasattr(a[1], 'value') else "Verification failed"
3849
+ # Truthy check
3850
+ is_truthy = True
3851
+ if hasattr(condition, 'value'):
3852
+ is_truthy = bool(condition.value)
3853
+ elif condition is NULL or condition is FALSE:
3854
+ is_truthy = False
3855
+ if not is_truthy:
3856
+ return EvaluationError(msg)
3857
+ return TRUE
3858
+
3859
+ self.builtins.update({
3860
+ "track_memory": Builtin(_track_memory, "track_memory"),
3861
+ "cache": Builtin(_cache, "cache"),
3862
+ "throttle": Builtin(_throttle, "throttle"),
3863
+ "audit": Builtin(_audit, "audit"),
3864
+ "verify": Builtin(_verify, "verify"),
3865
+ })
3832
3866
 
3833
3867
  def _register_main_entry_point_builtins(self):
3834
3868
  """Register builtins for main entry point pattern and continuous execution"""
@@ -4004,7 +4038,9 @@ class FunctionEvaluatorMixin:
4004
4038
  print(f"⚠️ on_exit hook error: {str(e)}")
4005
4039
 
4006
4040
  print(f"👋 Exiting with code {exit_code}")
4007
- sys.exit(exit_code)
4041
+ # SECURITY (H4): Raise SystemExit instead of calling sys.exit()
4042
+ # so it can be caught by the interpreter's top-level handler
4043
+ raise SystemExit(exit_code)
4008
4044
 
4009
4045
  def _on_start(*a):
4010
4046
  """
@@ -4167,23 +4203,19 @@ class FunctionEvaluatorMixin:
4167
4203
  """
4168
4204
  Run the current process as a background daemon.
4169
4205
 
4170
- Detaches from terminal and runs in background. On Unix systems, this
4171
- performs a double fork to properly daemonize. On Windows, it's a no-op.
4172
-
4173
- Usage:
4174
- if is_main() {
4175
- daemonize()
4176
- # Now running as daemon
4177
- run(my_server_task)
4178
- }
4179
-
4180
- Optional arguments:
4181
- daemonize() # Use defaults
4182
- daemonize(working_dir) # Set working directory
4206
+ SECURITY (C8): Requires ZEXUS_ALLOW_DAEMON=1 environment variable.
4207
+ Disabled by default to prevent untrusted scripts from forking.
4183
4208
  """
4184
4209
  import os
4185
4210
  import sys
4186
4211
 
4212
+ # Security gate: must be explicitly opted-in
4213
+ if os.environ.get('ZEXUS_ALLOW_DAEMON') != '1':
4214
+ return EvaluationError(
4215
+ "daemonize() is disabled for security. "
4216
+ "Set ZEXUS_ALLOW_DAEMON=1 environment variable to enable."
4217
+ )
4218
+
4187
4219
  # Check if we're on a Unix-like system
4188
4220
  if not hasattr(os, 'fork'):
4189
4221
  return EvaluationError("daemonize() is only supported on Unix-like systems")
@@ -4826,7 +4858,13 @@ class FunctionEvaluatorMixin:
4826
4858
  return String(value)
4827
4859
 
4828
4860
  def _env_set(*a):
4829
- """Set environment variable: env_set("VAR_NAME", "value")"""
4861
+ """Set environment variable: env_set("VAR_NAME", "value")
4862
+
4863
+ SECURITY (C9): Blocks modification of security-sensitive env vars
4864
+ (PATH, LD_PRELOAD, PYTHONPATH, etc.).
4865
+ """
4866
+ from ..object import BLOCKED_ENV_VARS
4867
+
4830
4868
  if len(a) != 2:
4831
4869
  return EvaluationError("env_set() takes 2 arguments: var_name, value")
4832
4870
 
@@ -4836,6 +4874,12 @@ class FunctionEvaluatorMixin:
4836
4874
  var_name = var_name_obj.value if isinstance(var_name_obj, String) else str(var_name_obj)
4837
4875
  value = value_obj.value if isinstance(value_obj, String) else str(value_obj)
4838
4876
 
4877
+ # Security: block sensitive environment variables
4878
+ if var_name.upper() in BLOCKED_ENV_VARS:
4879
+ return EvaluationError(
4880
+ f"env_set() denied: '{var_name}' is a protected environment variable"
4881
+ )
4882
+
4839
4883
  os.environ[var_name] = value
4840
4884
  return TRUE
4841
4885
 
@@ -4885,22 +4929,23 @@ class FunctionEvaluatorMixin:
4885
4929
  return String("strong")
4886
4930
 
4887
4931
  def _sanitize_input(*a):
4888
- """Sanitize user input by removing dangerous characters"""
4932
+ """Sanitize user input without corrupting data.
4933
+
4934
+ LI9: This is *not* an SQL-injection defense and must not attempt to
4935
+ strip SQL keywords (bypassable + corrupts user data). Prefer
4936
+ parameterized queries for DB APIs.
4937
+ """
4889
4938
  if len(a) != 1:
4890
4939
  return EvaluationError("sanitize_input() takes 1 argument")
4891
4940
 
4892
4941
  val = a[0]
4893
4942
  input_str = val.value if isinstance(val, String) else str(val)
4894
4943
 
4895
- # Remove potentially dangerous characters
4896
- # Remove HTML tags
4897
- sanitized = re.sub(r'<[^>]+>', '', input_str)
4898
- # Remove script tags
4899
- sanitized = re.sub(r'<script[^>]*>.*?</script>', '', sanitized, flags=re.IGNORECASE)
4900
- # Remove SQL injection patterns
4901
- sanitized = re.sub(r'(;|--|\'|\"|\bOR\b|\bAND\b)', '', sanitized, flags=re.IGNORECASE)
4902
-
4903
- return String(sanitized)
4944
+ sanitized = input_str.replace("\x00", "")
4945
+ sanitized = sanitized.replace("\r\n", "\n").replace("\r", "\n")
4946
+ # Preserve original trust level if we received a String.
4947
+ is_trusted = val.is_trusted if isinstance(val, String) else False
4948
+ return String(sanitized, sanitized_for="generic", is_trusted=is_trusted)
4904
4949
 
4905
4950
  def _validate_length(*a):
4906
4951
  """Validate string length: validate_length(value, min, max)"""