2025-12-29 03:06:49 +08:00
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
Secure code execution tools
|
|
|
|
|
Provides timeouts, resource limits and a sandbox environment
|
2025-12-29 03:06:49 +08:00
|
|
|
"""
|
|
|
|
|
import signal
|
|
|
|
|
import sys
|
|
|
|
|
import os
|
|
|
|
|
import threading
|
|
|
|
|
import traceback
|
|
|
|
|
from typing import Dict, Any, Optional, Tuple
|
|
|
|
|
from contextlib import contextmanager
|
|
|
|
|
|
|
|
|
|
from app.utils.logger import get_logger
|
|
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TimeoutError(Exception):
|
2026-04-06 16:47:36 +07:00
|
|
|
"""Code execution timeout exception"""
|
2025-12-29 03:06:49 +08:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
|
def timeout_context(seconds: int):
|
|
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
Code execution timeout context manager
|
2025-12-29 03:06:49 +08:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
Notice:
|
|
|
|
|
- Only works on Unix/Linux systems
|
|
|
|
|
- Only valid in the main thread, non-main threads will be downgraded to unlimited timeout
|
|
|
|
|
- Will downgrade to unlimited timeout on Windows
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
Args:
|
2026-04-06 16:47:36 +07:00
|
|
|
seconds: timeout (seconds)
|
2025-12-29 03:06:49 +08:00
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check if in main thread
|
2025-12-29 03:06:49 +08:00
|
|
|
is_main_thread = threading.current_thread() is threading.main_thread()
|
|
|
|
|
|
|
|
|
|
if sys.platform == 'win32':
|
2026-04-06 16:47:36 +07:00
|
|
|
# signal.alarm is not supported on Windows, only warnings can be logged
|
2025-12-29 03:06:49 +08:00
|
|
|
logger.warning("Windows does not support signal-based timeouts; execution time limits may not work")
|
|
|
|
|
yield
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not is_main_thread:
|
2026-04-06 16:47:36 +07:00
|
|
|
# Non-main threads cannot use signal. Warnings are logged but timeouts are not limited.
|
|
|
|
|
# logger.warning(f"Currently running in a non-main thread (thread: {threading.current_thread().name}),"
|
|
|
|
|
# f"signal timeout is not available, code execution may not limit the time")
|
2025-12-29 03:06:49 +08:00
|
|
|
yield
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def timeout_handler(signum, frame):
|
2026-04-08 07:27:26 +07:00
|
|
|
raise TimeoutError(f"Code execution timed out after {seconds} seconds")
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
try:
|
2026-04-06 16:47:36 +07:00
|
|
|
# Set up signal handler
|
2025-12-29 03:06:49 +08:00
|
|
|
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
|
|
|
|
signal.alarm(seconds)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
2026-04-06 16:47:36 +07:00
|
|
|
# Restore original signal handler
|
2025-12-29 03:06:49 +08:00
|
|
|
signal.alarm(0)
|
|
|
|
|
signal.signal(signal.SIGALRM, old_handler)
|
|
|
|
|
except ValueError as e:
|
2026-04-06 16:47:36 +07:00
|
|
|
# If signal setup fails (such as in some environments), log a warning but do not interrupt execution
|
2025-12-29 03:06:49 +08:00
|
|
|
logger.warning(f"Failed to set signal timeout: {str(e)}; execution will continue without timeout enforcement")
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def safe_exec_code(
|
|
|
|
|
code: str,
|
|
|
|
|
exec_globals: Dict[str, Any],
|
|
|
|
|
exec_locals: Optional[Dict[str, Any]] = None,
|
|
|
|
|
timeout: int = 30,
|
|
|
|
|
max_memory_mb: Optional[int] = None
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
Safe execution of Python code
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
Args:
|
2026-04-06 16:47:36 +07:00
|
|
|
code: Python code to execute
|
|
|
|
|
exec_globals: dictionary of global variables
|
|
|
|
|
exec_locals: dictionary of local variables (if None, exec_globals is used)
|
|
|
|
|
timeout: timeout (seconds), default 30 seconds
|
|
|
|
|
max_memory_mb: Maximum memory limit (MB), default 500MB
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
Returns:
|
2026-04-06 16:47:36 +07:00
|
|
|
Execution result dictionary, including:
|
|
|
|
|
- success: bool, whether the execution was successful
|
|
|
|
|
- error: str, error message (if failure)
|
|
|
|
|
- result: Any, execution result (if any)
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
Raises:
|
2026-04-06 16:47:36 +07:00
|
|
|
TimeoutError: If the code execution times out
|
2025-12-29 03:06:49 +08:00
|
|
|
"""
|
|
|
|
|
if exec_locals is None:
|
|
|
|
|
exec_locals = exec_globals
|
|
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Set memory limit (if supported)
|
2025-12-29 03:06:49 +08:00
|
|
|
if max_memory_mb is None:
|
2026-04-06 16:47:36 +07:00
|
|
|
max_memory_mb = 500 # Default 500MB
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
try:
|
2026-04-06 16:47:36 +07:00
|
|
|
# Note: resource.setrlimit is process level and affects the entire API process.
|
|
|
|
|
# The previous global limit of 500MB could prevent parallel strategies/threads from allocating memory.
|
|
|
|
|
# Only set if SAFE_EXEC_ENABLE_RLIMIT is explicitly turned on.
|
2025-12-29 03:06:49 +08:00
|
|
|
if sys.platform != 'win32' and os.getenv('SAFE_EXEC_ENABLE_RLIMIT', 'false').lower() == 'true':
|
|
|
|
|
try:
|
|
|
|
|
import resource
|
|
|
|
|
max_memory_bytes = max_memory_mb * 1024 * 1024
|
|
|
|
|
resource.setrlimit(resource.RLIMIT_AS, (max_memory_bytes, max_memory_bytes))
|
|
|
|
|
logger.debug(f"Memory limit set: {max_memory_mb}MB (SAFE_EXEC_ENABLE_RLIMIT enabled)")
|
|
|
|
|
except (ImportError, ValueError, OSError) as e:
|
|
|
|
|
logger.warning(f"Failed to set memory limit: {str(e)}")
|
|
|
|
|
else:
|
|
|
|
|
logger.debug("No resource memory limit (SAFE_EXEC_ENABLE_RLIMIT disabled or unsupported platform)")
|
|
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# On Windows, timeout_context doesn't really limit the time
|
|
|
|
|
# But a warning will be logged
|
2025-12-29 03:06:49 +08:00
|
|
|
with timeout_context(timeout):
|
|
|
|
|
exec(code, exec_globals, exec_locals)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'success': True,
|
|
|
|
|
'error': None,
|
|
|
|
|
'result': None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except MemoryError as e:
|
2026-04-08 07:27:26 +07:00
|
|
|
error_msg = f"Code execution exceeded the memory limit of {max_memory_mb}MB"
|
2025-12-29 03:06:49 +08:00
|
|
|
logger.error(f"Code execution out of memory (limit={max_memory_mb}MB)")
|
|
|
|
|
return {
|
|
|
|
|
'success': False,
|
|
|
|
|
'error': error_msg,
|
|
|
|
|
'result': None
|
|
|
|
|
}
|
|
|
|
|
except TimeoutError as e:
|
|
|
|
|
error_msg = str(e)
|
|
|
|
|
logger.error(f"Code execution timed out (timeout={timeout}s)")
|
|
|
|
|
return {
|
|
|
|
|
'success': False,
|
|
|
|
|
'error': error_msg,
|
|
|
|
|
'result': None
|
|
|
|
|
}
|
|
|
|
|
except Exception as e:
|
2026-04-08 07:27:26 +07:00
|
|
|
error_msg = f"Code execution error: {str(e)}\n{traceback.format_exc()}"
|
2025-12-29 03:06:49 +08:00
|
|
|
logger.error(f"Code execution error: {str(e)}")
|
|
|
|
|
logger.error(traceback.format_exc())
|
|
|
|
|
return {
|
|
|
|
|
'success': False,
|
|
|
|
|
'error': error_msg,
|
|
|
|
|
'result': None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]:
|
|
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
Verify code security (basic check)
|
2025-12-29 03:06:49 +08:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
Check your code for dangerous function calls or imports
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
Args:
|
2026-04-06 16:47:36 +07:00
|
|
|
code: Python code to check
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
(is_safe: bool, error_message: Optional[str])
|
|
|
|
|
"""
|
|
|
|
|
import ast
|
|
|
|
|
import re
|
|
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Dangerous keywords and function names
|
2025-12-29 03:06:49 +08:00
|
|
|
dangerous_patterns = [
|
2026-04-06 16:47:36 +07:00
|
|
|
# System command execution
|
2025-12-29 03:06:49 +08:00
|
|
|
r'\bos\.system\b',
|
|
|
|
|
r'\bos\.popen\b',
|
|
|
|
|
r'\bos\.spawn\b',
|
|
|
|
|
r'\bos\.exec\b',
|
|
|
|
|
r'\bos\.fork\b',
|
|
|
|
|
r'\bsubprocess\b',
|
|
|
|
|
r'\bcommands\b',
|
2026-04-06 16:47:36 +07:00
|
|
|
# code execution
|
2025-12-29 03:06:49 +08:00
|
|
|
r'\b__import__\s*\(',
|
|
|
|
|
r'\beval\s*\(',
|
|
|
|
|
r'\bexec\s*\(',
|
|
|
|
|
r'\bcompile\s*\(',
|
2026-04-06 16:47:36 +07:00
|
|
|
# File operations
|
2025-12-29 03:06:49 +08:00
|
|
|
r'\bopen\s*\(',
|
|
|
|
|
r'\bfile\s*\(',
|
|
|
|
|
r'\b__builtins__\b',
|
2026-04-06 16:47:36 +07:00
|
|
|
# module import
|
2025-12-29 03:06:49 +08:00
|
|
|
r'\bimport\s+os\b',
|
|
|
|
|
r'\bimport\s+sys\b',
|
|
|
|
|
r'\bimport\s+subprocess\b',
|
|
|
|
|
r'\bimport\s+pymysql\b',
|
|
|
|
|
r'\bimport\s+sqlite3\b',
|
|
|
|
|
r'\bimport\s+requests\b',
|
|
|
|
|
r'\bimport\s+urllib\b',
|
|
|
|
|
r'\bimport\s+http\b',
|
|
|
|
|
r'\bimport\s+socket\b',
|
|
|
|
|
r'\bimport\s+ftplib\b',
|
|
|
|
|
r'\bimport\s+telnetlib\b',
|
|
|
|
|
r'\bimport\s+pickle\b',
|
|
|
|
|
r'\bimport\s+cpickle\b',
|
|
|
|
|
r'\bimport\s+marshal\b',
|
|
|
|
|
r'\bimport\s+ctypes\b',
|
|
|
|
|
r'\bimport\s+multiprocessing\b',
|
|
|
|
|
r'\bimport\s+threading\b',
|
|
|
|
|
r'\bimport\s+concurrent\b',
|
2026-04-06 16:47:36 +07:00
|
|
|
# Reflection and metaprogramming (possibly used to bypass restrictions)
|
2025-12-29 03:06:49 +08:00
|
|
|
r'\bgetattr\s*\(.*__import__',
|
|
|
|
|
r'\bgetattr\s*\(.*eval',
|
|
|
|
|
r'\bgetattr\s*\(.*exec',
|
|
|
|
|
r'\bsetattr\s*\(',
|
|
|
|
|
r'\b__getattribute__\b',
|
|
|
|
|
r'\b__setattr__\b',
|
|
|
|
|
r'\b__dict__\b',
|
|
|
|
|
r'\bglobals\s*\(',
|
|
|
|
|
r'\blocals\s*\(',
|
|
|
|
|
r'\bdir\s*\(',
|
2026-04-06 16:47:36 +07:00
|
|
|
r'\btype\s*\(.*\)\s*\(', # type() may be used to create new types
|
2025-12-29 03:06:49 +08:00
|
|
|
r'\b__class__\b',
|
|
|
|
|
r'\b__bases__\b',
|
|
|
|
|
r'\b__subclasses__\b',
|
|
|
|
|
r'\b__mro__\b',
|
|
|
|
|
r'\b__init__\b.*__import__',
|
|
|
|
|
r'\b__new__\b.*__import__',
|
2026-04-06 16:47:36 +07:00
|
|
|
# Other dangerous operations
|
2025-12-29 03:06:49 +08:00
|
|
|
r'\b__builtins__\s*\[',
|
|
|
|
|
r'\b__builtins__\s*\.',
|
|
|
|
|
r'\b__import__\s*\(',
|
|
|
|
|
r'\bimportlib\b',
|
|
|
|
|
r'\bimp\b',
|
|
|
|
|
]
|
|
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check your code for dangerous patterns
|
2025-12-29 03:06:49 +08:00
|
|
|
for pattern in dangerous_patterns:
|
|
|
|
|
if re.search(pattern, code):
|
2026-04-08 07:27:26 +07:00
|
|
|
return False, f"Dangerous code pattern detected: {pattern}"
|
2025-12-29 03:06:49 +08:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Try parsing the AST, checking for dangerous nodes
|
2025-12-29 03:06:49 +08:00
|
|
|
try:
|
|
|
|
|
tree = ast.parse(code)
|
|
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# List of dangerous modules (extended)
|
2025-12-29 03:06:49 +08:00
|
|
|
dangerous_modules = [
|
|
|
|
|
'os', 'sys', 'subprocess', 'pymysql', 'sqlite3',
|
|
|
|
|
'requests', 'urllib', 'http', 'socket', 'ftplib', 'telnetlib',
|
|
|
|
|
'pickle', 'cpickle', 'marshal', 'ctypes',
|
|
|
|
|
'multiprocessing', 'threading', 'concurrent',
|
|
|
|
|
'importlib', 'imp', 'builtins'
|
|
|
|
|
]
|
|
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# List of dangerous functions (extended)
|
|
|
|
|
# Note: hasattr is safe and is only used to check attributes, not to access
|
2025-12-29 03:06:49 +08:00
|
|
|
dangerous_functions = [
|
|
|
|
|
'eval', 'exec', 'compile', '__import__',
|
2026-04-06 16:47:36 +07:00
|
|
|
'getattr', 'setattr', 'delattr', # hasattr has been removed, it is safe
|
2025-12-29 03:06:49 +08:00
|
|
|
'globals', 'locals', 'vars', 'dir', 'type'
|
|
|
|
|
]
|
|
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check for dangerous function calls
|
2025-12-29 03:06:49 +08:00
|
|
|
for node in ast.walk(tree):
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check for calls to dangerous functions
|
2025-12-29 03:06:49 +08:00
|
|
|
if isinstance(node, ast.Call):
|
|
|
|
|
if isinstance(node.func, ast.Name):
|
|
|
|
|
func_name = node.func.id
|
|
|
|
|
if func_name in dangerous_functions:
|
2026-04-08 07:27:26 +07:00
|
|
|
return False, f"Dangerous function call detected: {func_name}()"
|
2025-12-29 03:06:49 +08:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check if there are calls to os.system etc.
|
2025-12-29 03:06:49 +08:00
|
|
|
if isinstance(node.func, ast.Attribute):
|
|
|
|
|
if isinstance(node.func.value, ast.Name):
|
|
|
|
|
if node.func.value.id in dangerous_modules:
|
2026-04-08 07:27:26 +07:00
|
|
|
return False, f"Dangerous module call detected: {node.func.value.id}.{node.func.attr}"
|
2025-12-29 03:06:49 +08:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check if there are bypass methods such as getattr(builtins, '__import__')
|
2025-12-29 03:06:49 +08:00
|
|
|
if isinstance(node.func, ast.Name) and node.func.id == 'getattr':
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check the parameters of getattr
|
2025-12-29 03:06:49 +08:00
|
|
|
if len(node.args) >= 2:
|
|
|
|
|
if isinstance(node.args[0], ast.Name) and node.args[0].id in ['builtins', '__builtins__']:
|
|
|
|
|
if isinstance(node.args[1], ast.Constant) and node.args[1].value in dangerous_functions:
|
2026-04-08 07:27:26 +07:00
|
|
|
return False, f"Detected an attempt to bypass restrictions via getattr: getattr({node.args[0].id}, '{node.args[1].value}')"
|
2025-12-29 03:06:49 +08:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check the import statement: the use of import is prohibited in user scripts (security dependencies are uniformly injected by the platform)
|
2025-12-29 03:06:49 +08:00
|
|
|
for node in ast.walk(tree):
|
|
|
|
|
if isinstance(node, ast.Import):
|
2026-04-08 07:27:26 +07:00
|
|
|
return False, "Import statements are not allowed in scripts. Use the platform-provided objects such as pd and np directly."
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
if isinstance(node, ast.ImportFrom):
|
2026-04-08 07:27:26 +07:00
|
|
|
return False, "Import statements are not allowed in scripts. Use the platform-provided objects such as pd and np directly."
|
2025-12-29 03:06:49 +08:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check if there is an attempt to access __builtins__
|
2025-12-29 03:06:49 +08:00
|
|
|
for node in ast.walk(tree):
|
|
|
|
|
if isinstance(node, ast.Attribute):
|
|
|
|
|
if isinstance(node.attr, str) and node.attr.startswith('__') and node.attr.endswith('__'):
|
|
|
|
|
if node.attr in ['__builtins__', '__import__', '__class__', '__bases__', '__subclasses__', '__mro__']:
|
2026-04-06 16:47:36 +07:00
|
|
|
# Check if used in dangerous context
|
2025-12-29 03:06:49 +08:00
|
|
|
if isinstance(node.value, ast.Name) and node.value.id in ['builtins', '__builtins__']:
|
2026-04-08 07:27:26 +07:00
|
|
|
return False, f"Dangerous attribute access detected: {node.value.id}.{node.attr}"
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
except SyntaxError as e:
|
2026-04-08 07:27:26 +07:00
|
|
|
return False, f"Code syntax error: {str(e)}"
|
2025-12-29 03:06:49 +08:00
|
|
|
except Exception as e:
|
2026-04-06 16:47:36 +07:00
|
|
|
# If AST parsing fails, log a warning but allow to continue (possibly incomplete code)
|
2025-12-29 03:06:49 +08:00
|
|
|
logger.warning(f"AST parse failed; skipping safety checks: {str(e)}")
|
|
|
|
|
|
|
|
|
|
return True, None
|