Skip to content
Posts en inglés. Usá el traductor del navegador para leerlos en tu idioma.

A Guide to Securely Executing LLM-Generated Code

Yammbo
· 9 min read
ai code security generative ai code code execution safety containerized execution microvms
A Guide to Securely Executing LLM-Generated Code

Large Language Models (LLMs) are increasingly capable of generating functional code snippets, scripts, and even entire applications. While this accelerates development, it introduces a unique challenge: how do you safely execute code that an AI wrote? Traditional sandboxing strategies, designed to contain malicious human actors, often miss the mark when dealing with LLM-generated code. This tutorial will guide you through understanding the distinct threat model of AI-generated code and implementing practical, cost-effective sandboxing techniques tailored to its common failure modes.

Understanding the Unique Threat Model of LLM-Generated Code

When you execute code written by an LLM, the primary concern isn't typically a deliberate attempt to breach your system. Unlike code submitted by an untrusted human user with malicious intent, an LLM has no "intent" at all. Its failures are usually accidental: a misunderstanding of the prompt, an incorrect API call, a missing dependency, or resource exhaustion. These are bugs, not attacks.

The traditional "isolation ladder" — moving from bare processes to containers, microVMs, and hardware isolation — is often presented as a climb to greater security. However, this ladder is built to answer a question you mostly don't have when dealing with LLM output. It assumes an adversary with patience, a plan, and a desire to escape. LLM-generated code lacks these attributes. Therefore, focusing solely on the strength of the isolation boundary against a determined attacker can lead to over-engineering and unnecessary costs, while leaving you vulnerable to the more common, benign failures.

Instead, your threat model should be a probability distribution of failure classes. The vast majority of issues will stem from accidents. A robust sandboxing strategy for LLM-generated code prioritizes containment of these accidental failures through careful configuration and resource management, rather than solely focusing on preventing sophisticated breakouts.

Implementing Foundational Containment Policies

Effective sandboxing for LLM-generated code begins with configuring robust containment policies that address common accidental failures. These policies can often be applied even at the level of a bare subprocess, providing significant protection without the overhead of more complex isolation technologies.

  1. Set Resource Limits: Prevent runaway processes from consuming all available system resources. This includes:
    • CPU Limits: Restrict the percentage of CPU time a process can use (e.g., using cgroups on Linux or job objects on Windows).
    • Memory Limits: Define the maximum RAM a process can allocate. Exceeding this should terminate the process gracefully.
    • Disk Usage Limits: Cap the amount of temporary storage or log files a process can create.

    Verification: Write a simple script that attempts to allocate a large amount of memory or enter an infinite loop, and observe if your configured limits are enforced, terminating the process.

  2. Enforce Execution Timeouts: LLM-generated code might get stuck in an infinite loop or take an unexpectedly long time to complete. A strict timeout ensures that such processes are terminated, preventing resource lock-up.

    Verification: Execute a script with an intentional delay (e.g., time.sleep(60)) that exceeds your timeout, confirming it's stopped.

  3. Implement Egress Policies: Control what external resources the generated code can access. By default, restrict all network access, then explicitly whitelist only necessary domains or IP addresses. This prevents unintended data exfiltration or access to sensitive internal services.

    Verification: Try to make an HTTP request to an external website (e.g., requests.get("http://example.com")) from within the sandbox. It should fail if egress is properly restricted.

  4. Cap Output and Log Sizes: Malfunctioning code can generate enormous amounts of output or log data. Implement caps to prevent disk exhaustion and make debugging manageable.

    Verification: Run a script that prints a massive string or writes a large file. Confirm that the output is truncated or the file creation is stopped at your defined limit.

  5. Broker Credentials and Environment Variables: Never expose sensitive API keys, database credentials, or other secrets directly to the sandboxed environment. Instead, use a secure brokering mechanism that injects only the necessary, scoped credentials at runtime, or proxies requests through a trusted service.

    Verification: Attempt to access an environment variable that should be restricted or use a hardcoded sensitive value. The attempt should fail or be blocked.

The goal here is to define a "ceiling" for the code's behavior. A limit that explicitly refuses an operation and names itself (e.g., "Memory limit exceeded") is far more useful for debugging than one that silently kills the program.

Managing Code Dependencies in Isolated Environments

One of the most common vectors for accidental failure in LLM-generated code is its dependency path. The model might assume certain libraries are present, request non-existent packages, or specify incompatible versions. Managing these dependencies in an isolated, controlled manner is crucial for stability and security.

  1. Utilize Virtual Environments: For language-specific dependencies (e.g., Python's venv or Node.js's node_modules), always execute code within a dedicated virtual environment. This prevents conflicts with system-wide packages and ensures that each execution context is clean and predictable.
    python3 -m venv my_sandbox_envsource my_sandbox_env/bin/activatepip install <required_packages>python my_llm_script.pydeactivate

    Verification: Run code that requires a specific package. Then, deactivate the virtual environment and try to run it again; it should fail if the package isn't globally installed.

  2. Containerize Execution: For more robust isolation and reproducible environments, consider using container technologies like Docker. Each LLM code execution can happen within a fresh container image, pre-loaded with a minimal set of necessary dependencies. This provides a clean slate every time, mitigating "dependency hell."
    # Example DockerfileFROM python:3.9-slim-busterWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY my_llm_script.py .CMD ["python", "my_llm_script.py"]

    Verification: Build and run a container with specific dependencies. Then, try to run code requiring a different, uninstalled dependency; it should fail within the container.

  3. Control Package Installation: Prevent the generated code from arbitrarily installing new packages from public repositories. This can introduce vulnerabilities, consume excessive resources, or pull in unwanted dependencies.
    • Pre-install Approved Packages: Provide a curated set of libraries within your base environment or container image.
    • Restrict Package Manager Access: Configure the sandbox to block calls to pip install, npm install, or similar commands.
    • Use Private Registries: If dynamic package installation is unavoidable, direct package managers to a private, audited registry.

    Verification: Attempt to run pip install some_random_package from within your sandboxed environment. It should be blocked or fail.

By carefully managing dependencies, you reduce the surface area for accidental failures and make debugging much more straightforward. An explicit error message about a missing or forbidden dependency is far preferable to a silent crash or unexpected behavior.

Optimizing for Cost with Ephemeral Execution and Idle Time Management

A significant, often overlooked, cost factor in sandboxing LLM-generated code is idle time. Unlike traditional sandboxing where a box is used once and discarded, an agent loop might generate and execute code repeatedly, with long pauses in between. The environment still incurs costs while waiting for the next execution. Optimizing for this "rate" rather than just isolation "content" is crucial for efficiency.

  1. Embrace Ephemeral Environments: Instead of maintaining long-lived sandbox instances, design your system to spin up a fresh, isolated environment (e.g., a container or a virtual machine) just before execution and tear it down immediately after. This "serverless" approach ensures you only pay for compute resources when code is actively running.

    Verification: Monitor resource usage (CPU, memory) of your sandboxing infrastructure. You should see spikes during execution and near-zero usage during idle periods.

  2. Implement Explicit Pause/Resume Mechanisms: Relying on idle timers to detect when a sandbox can be paused or terminated is inefficient. By the time a timer triggers, you've already paid for significant idle time. Instead, integrate explicit pause or cleanup calls directly into the code path that manages the LLM's execution loop.
    • After collecting the output from an execution, explicitly signal the environment to pause, checkpoint, or terminate.
    • If using technologies that support it (like Firecracker microVMs or certain container runtimes), leverage their snapshotting or pause capabilities to quickly freeze and resume execution states.

    Verification: Measure the time between code execution completion and the environment's deallocation or pause. Aim for this interval to be as short as possible, ideally milliseconds, not seconds or minutes.

  3. Pre-warm Base Images: While ephemeral environments are cost-effective, the startup time can add latency. Mitigate this by pre-warming base container images or VM templates that contain all common dependencies. This reduces the time it takes to instantiate a new sandbox.

    Verification: Benchmark the startup time of a fresh sandbox instance. Compare it to a scenario where you use a pre-warmed image to see the improvement.

By focusing on rapid instantiation, explicit termination, and efficient resource allocation, you can dramatically reduce the operational costs associated with safely running LLM-generated code, especially in high-frequency or long-running agent scenarios.

When to Consider Hardware Boundaries for Data-Driven Attacks

While LLM-generated code typically fails accidentally, there's a critical scenario where a hardware boundary becomes essential: when an attacker can inject malicious instructions into the data that your LLM processes. This is a supply chain attack, where the LLM itself becomes an unwitting accomplice.

Imagine your LLM is tasked with summarizing a document or processing user input. An attacker could embed commands within that input data, designed to be interpreted and executed by the generated code. For example, a seemingly benign data file might contain a hidden shell command that the LLM's Python script then executes. In these cases, the "author" of the malicious intent isn't the LLM, but the external data source.

For such scenarios, where the input path to your LLM is highly untrusted and could carry hidden payloads, investing in a stronger hardware boundary is justified. Technologies like microVMs (e.g., Firecracker) or even dedicated physical hardware can provide the necessary isolation to contain these sophisticated injection attacks. These boundaries make it significantly harder for malicious code, even if triggered by the LLM, to escape its designated environment and compromise the host system or other tenants.

The key is to size your isolation boundary to your input path's trustworthiness, not just to the worst thing you can imagine. If your LLM processes highly sensitive or untrusted external data, a hardware boundary acts as a crucial last line of defense against attacks that bypass software-level containment by manipulating the LLM's own output.

Safely executing LLM-generated code requires a shift in perspective from traditional threat models. By focusing on containing accidental failures through robust configuration, managing dependencies in isolated environments, and optimizing for idle time, you can build a secure and cost-effective execution layer. Remember to reserve hardware boundaries for critical supply chain risks where untrusted data could inject malicious commands. For building and deploying powerful web applications, consider exploring Yammbo Web.