File size: 8,282 Bytes
a89f25d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import docker
from docker.errors import DockerException, ContainerError, ImageNotFound, APIError
import time
import logging
from typing import Optional
from sandbox.models import ExecutionRequest, ExecutionResponse, SandboxConfig, Language
from sandbox.language_runners import LanguageRunner

logger = logging.getLogger(__name__)


class SandboxExecutor:
    """
    Executes code in isolated Docker containers with security controls.
    
    Features:
    - Resource limits (CPU, memory)
    - Network isolation
    - Automatic cleanup
    - Timeout enforcement
    - Read-only filesystem
    """
    
    def __init__(self, config: Optional[SandboxConfig] = None):
        self.config = config or SandboxConfig()
        try:
            self.client = docker.from_env()
            # Test Docker connection
            self.client.ping()
            logger.info("Docker client initialized successfully")
        except DockerException as e:
            logger.error(f"Failed to initialize Docker client: {e}")
            raise RuntimeError(
                "Docker is not available. Please ensure Docker is running and accessible."
            ) from e
    
    def _prepare_container_config(
        self, 
        request: ExecutionRequest,
        runner_config: dict
    ) -> dict:
        """Prepare Docker container configuration with security settings"""
        
        # Calculate resource limits
        memory_limit = f"{request.memory_limit}m"
        cpu_quota = 50000  # 0.5 CPU cores (out of 100000)
        
        container_config = {
            "image": runner_config["image"],
            "command": runner_config["command"] + [request.code],
            "stdin_open": bool(request.stdin),
            "detach": True,
            "remove": False,  # We'll remove manually after getting logs
            
            # Security settings
            "network_mode": "none" if not self.config.enable_network else "bridge",
            "read_only": self.config.read_only_root,
            "security_opt": ["no-new-privileges"],
            
            # Resource limits
            "mem_limit": memory_limit,
            "memswap_limit": memory_limit,  # Disable swap
            "cpu_quota": cpu_quota,
            "cpu_period": 100000,
            
            # Prevent fork bombs
            "pids_limit": 50,
            
            # Working directory
            "working_dir": "/tmp",
            
            # User (non-root when possible)
            "user": "nobody" if runner_config["image"] != "bash:5.2-alpine" else None,
        }
        
        return container_config
    
    def execute(self, request: ExecutionRequest) -> ExecutionResponse:
        """
        Execute code in an isolated container.
        
        Args:
            request: Execution request with code and parameters
            
        Returns:
            ExecutionResponse with stdout, stderr, and execution metadata
        """
        start_time = time.time()
        container = None
        
        try:
            # Get language-specific configuration
            runner_config = LanguageRunner.get_runner_config(request.language)
            
            # Prepare container configuration
            container_config = self._prepare_container_config(request, runner_config)
            
            # Pull image if not available
            try:
                self.client.images.get(runner_config["image"])
            except ImageNotFound:
                logger.info(f"Pulling image {runner_config['image']}...")
                self.client.images.pull(runner_config["image"])
            
            # Create and start container
            logger.info(f"Executing {request.language} code in container")
            container = self.client.containers.create(**container_config)
            container.start()
            
            # Provide stdin if specified
            if request.stdin:
                sock = container.attach_socket(params={'stdin': 1, 'stream': 1})
                sock._sock.sendall(request.stdin.encode())
                sock.close()
            
            # Wait for container to finish with timeout
            try:
                result = container.wait(timeout=request.timeout)
                exit_code = result.get("StatusCode", -1)
                error_msg = None
            except Exception as e:
                # Timeout or other error
                logger.warning(f"Container execution timeout or error: {e}")
                container.stop(timeout=1)
                exit_code = 124  # Timeout exit code
                error_msg = f"Execution timed out after {request.timeout} seconds"
            
            # Get logs (stdout and stderr combined)
            try:
                logs = container.logs(stdout=True, stderr=True).decode('utf-8', errors='replace')
                
                # Truncate if too large
                if len(logs) > self.config.max_output_size:
                    logs = logs[:self.config.max_output_size] + "\n... (output truncated)"
                
                # Try to separate stdout and stderr (Docker combines them)
                stdout = logs
                stderr = ""
                
                # If there's an error, try to extract stderr
                if exit_code != 0:
                    stderr = logs
                    stdout = ""
                    
            except Exception as e:
                logger.error(f"Failed to get container logs: {e}")
                stdout = ""
                stderr = str(e)
            
            execution_time = time.time() - start_time
            
            return ExecutionResponse(
                stdout=stdout,
                stderr=stderr,
                exit_code=exit_code,
                execution_time=round(execution_time, 3),
                error=error_msg
            )
            
        except ImageNotFound as e:
            logger.error(f"Image not found: {e}")
            return ExecutionResponse(
                stdout="",
                stderr="",
                exit_code=-1,
                execution_time=time.time() - start_time,
                error=f"Language image not available: {runner_config['image']}"
            )
            
        except ContainerError as e:
            logger.error(f"Container error: {e}")
            return ExecutionResponse(
                stdout="",
                stderr=str(e),
                exit_code=e.exit_status,
                execution_time=time.time() - start_time,
                error="Container execution failed"
            )
            
        except APIError as e:
            logger.error(f"Docker API error: {e}")
            return ExecutionResponse(
                stdout="",
                stderr="",
                exit_code=-1,
                execution_time=time.time() - start_time,
                error=f"Docker API error: {str(e)}"
            )
            
        except Exception as e:
            logger.error(f"Unexpected error during execution: {e}", exc_info=True)
            return ExecutionResponse(
                stdout="",
                stderr="",
                exit_code=-1,
                execution_time=time.time() - start_time,
                error=f"Unexpected error: {str(e)}"
            )
            
        finally:
            # Always cleanup container
            if container:
                try:
                    container.remove(force=True)
                    logger.debug(f"Container {container.id[:12]} removed")
                except Exception as e:
                    logger.warning(f"Failed to remove container: {e}")
    
    def cleanup_all(self):
        """Remove all stopped containers (maintenance task)"""
        try:
            containers = self.client.containers.list(all=True, filters={"status": "exited"})
            for container in containers:
                try:
                    container.remove()
                    logger.info(f"Cleaned up container {container.id[:12]}")
                except Exception as e:
                    logger.warning(f"Failed to remove container {container.id[:12]}: {e}")
        except Exception as e:
            logger.error(f"Failed to cleanup containers: {e}")