Skip to content

Commit ea8a018

Browse files
authored
Merge pull request #17 from usnavy13/secuirty-fixes
refactor: Enhance Docker image retrieval logic in ContainerManager
2 parents 6eb7562 + ebf9169 commit ea8a018

1 file changed

Lines changed: 81 additions & 7 deletions

File tree

src/services/container/manager.py

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Any, Dict, List, Optional, Tuple
99

1010
import structlog
11+
import docker.types
1112
from docker.errors import DockerException, ImageNotFound
1213
from 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

Comments
 (0)