88from typing import Any , Dict , List , Optional , Tuple
99
1010import structlog
11+ import docker .types
1112from docker .errors import DockerException , ImageNotFound
1213from docker .models .containers import Container
1314
@@ -55,8 +56,61 @@ def reset_initialization(self) -> None:
5556 self ._executor = None
5657
5758 def get_image_for_language (self , language : str ) -> str :
58- """Get Docker image for a programming language."""
59- return settings .get_image_for_language (language .lower ().strip ())
59+ """Get Docker image for a programming language.
60+
61+ Uses fallback logic to find available images:
62+ 1. Configured image from settings/env (e.g., DOCKER_IMAGE_REGISTRY)
63+ 2. Local build prefix: code-interpreter/<lang>:latest
64+ 3. GHCR prefix: ghcr.io/usnavy13/librecodeinterpreter/<lang>:latest
65+ """
66+ lang = language .lower ().strip ()
67+
68+ # Get the configured image name
69+ configured_image = settings .get_image_for_language (lang )
70+
71+ # Build list of fallback images to try
72+ # Extract the language-specific part (e.g., "python" from "registry/python:tag")
73+ lang_part = configured_image .split ("/" )[- 1 ] # e.g., "python:latest"
74+
75+ fallback_images = [
76+ configured_image , # First: configured image
77+ f"code-interpreter/{ lang_part } " , # Second: local build
78+ f"ghcr.io/usnavy13/librecodeinterpreter/{ lang_part } " , # Third: GHCR
79+ ]
80+
81+ # Remove duplicates while preserving order
82+ seen = set ()
83+ unique_images = []
84+ for img in fallback_images :
85+ if img not in seen :
86+ seen .add (img )
87+ unique_images .append (img )
88+
89+ # Check which image exists locally
90+ if self .is_available ():
91+ for image in unique_images :
92+ try :
93+ self .client .images .get (image )
94+ if image != configured_image :
95+ logger .info (f"Using fallback image { image } for language { lang } " )
96+ return image
97+ except ImageNotFound :
98+ continue
99+ except Exception :
100+ continue
101+
102+ # No local image found - fail fast with clear error
103+ tried_images = ", " .join (unique_images )
104+ error_msg = (
105+ f"No Docker image found for language '{ lang } '. "
106+ f"Tried: { tried_images } . "
107+ f"Please build images with 'docker compose build' or pull from GHCR."
108+ )
109+ logger .error (error_msg )
110+ raise ImageNotFound (error_msg )
111+
112+ # Docker not available, return configured (will fail later with better error)
113+ return configured_image
60114
61115 def get_user_id_for_language (self , language : str ) -> int :
62116 """Get the user ID for a language container."""
@@ -123,6 +177,10 @@ def create_container(
123177 use_wan_access = settings .enable_wan_access
124178
125179 # Security hardening: paths to mask to prevent host info leakage
180+ # Note: MaskedPaths/ReadonlyPaths are not supported by docker-py 7.1.0.
181+ # Instead, we use bind mounts to /dev/null for critical paths like
182+ # /proc/kallsyms and /proc/modules (see "mounts" in container_config).
183+ # The list below is kept for documentation purposes.
126184 hardening_config : Dict [str , Any ] = {}
127185 if settings .container_mask_host_info :
128186 hardening_config ["masked_paths" ] = [
@@ -134,6 +192,8 @@ def create_container(
134192 "/proc/keys" ,
135193 "/proc/timer_list" ,
136194 "/proc/sched_debug" ,
195+ "/proc/kallsyms" , # Kernel symbol addresses (KASLR bypass) - masked via bind mount
196+ "/proc/modules" , # Loaded kernel modules - masked via bind mount
137197 "/sys/firmware" ,
138198 "/sys/kernel/security" ,
139199 "/etc/machine-id" , # Unique machine identifier
@@ -174,11 +234,10 @@ def create_container(
174234 pass
175235
176236 # Build container config
177- # Note: MaskedPaths/ReadonlyPaths require Docker API >=1.44 and
178- # are not supported by docker-py. Path masking would require either:
179- # 1. Custom seccomp profile
180- # 2. gVisor/kata container runtime
181- # 3. Custom container image modifications
237+ # Security hardening applied:
238+ # - ulimits: nproc and nofile limits to prevent fork bombs and FD exhaustion
239+ # Note: /proc/kallsyms and /proc/modules masking would require MaskedPaths
240+ # (not supported by docker-py) or a custom seccomp profile.
182241 container_config : Dict [str , Any ] = {
183242 "image" : image ,
184243 "name" : container_name ,
@@ -194,6 +253,21 @@ def create_container(
194253 "cap_add" : ["CHOWN" , "DAC_OVERRIDE" , "FOWNER" , "SETGID" , "SETUID" ],
195254 "read_only" : False ,
196255 "tmpfs" : {"/tmp" : "noexec,nosuid,size=100m" },
256+ "ulimits" : [
257+ docker .types .Ulimit (
258+ name = "nproc" ,
259+ soft = settings .max_processes ,
260+ hard = settings .max_processes ,
261+ ),
262+ docker .types .Ulimit (
263+ name = "nofile" ,
264+ soft = settings .max_open_files ,
265+ hard = settings .max_open_files ,
266+ ),
267+ ],
268+ # Note: /proc/kallsyms and /proc/modules masking requires MaskedPaths
269+ # which docker-py doesn't support. Bind mounts to /proc are blocked by runc.
270+ # Alternative: use a custom seccomp profile or accept the limitation.
197271 "environment" : env ,
198272 "labels" : labels ,
199273 "hostname" : settings .container_generic_hostname ,
0 commit comments