Running untrusted code in Functions

Modal provides two primitives for running untrusted code: Restricted Functions and Sandboxes. While both can be used for running untrusted code, they serve different purposes: Sandboxes provide a container-like interface while Restricted Functions provide an interface similar to a traditional Function.

Restricted Functions are useful for executing:

  • Code generated by language models (LLMs)
  • User-submitted code in interactive environments
  • Third-party plugins or extensions

Using restrict_modal_access

To restrict a Function’s access to Modal resources, set restrict_modal_access=True on the Function definition:

import modal

app = modal.App()

@app.function(restrict_modal_access=True)
def run_untrusted_code(code_input: str):
    # This nunction cannot access Modal resources
    return eval(code_input)

When restrict_modal_access is enabled:

  • The Function cannot access Modal resources (Queues, Dicts, etc.)
  • The Function cannot call other Functions
  • The Function cannot access Modal’s internal APIs

Comparison with Sandboxes

While both restrict_modal_access and Sandboxes can be used for running untrusted code, they serve different purposes:

FeatureRestricted FunctionSandbox
StateStatelessStateful
InterfaceFunction-likeContainer-like
SetupSimple decoratorRequires explicit creation/termination
Use caseQuick, isolated code executionInteractive development, long-running sessions

Best Practices

When running untrusted code, consider these additional security measures:

  1. Use max_inputs=1 to ensure each container only handles one request. Containers that get reused could cause information leakage between users.
@app.function(restrict_modal_access=True, max_inputs=1)
def isolated_function(input_data):
    # Each input gets a fresh container
    return process(input_data)
  1. Set appropriate timeouts to prevent long-running operations:
@app.function(
    restrict_modal_access=True,
    timeout=30,  # 30 second timeout
    max_inputs=1
)
def time_limited_function(input_data):
    return process(input_data)
  1. Consider using block_network=True to prevent the container from making outbound network requests:
@app.function(
    restrict_modal_access=True,
    block_network=True,
    max_inputs=1
)
def network_isolated_function(input_data):
    return process(input_data)

Example: Running LLM-generated Code

Below is a complete example of running code generated by a language model:

import modal

app = modal.App("restricted-access-example")


@app.function(restrict_modal_access=True, max_inputs=1, timeout=30, block_network=True)
def run_llm_code(generated_code: str):
    try:
        # Create a restricted environment
        execution_scope = {}

        # Execute the generated code
        exec(generated_code, execution_scope)

        # Return the result if it exists
        return execution_scope.get("result", None)
    except Exception as e:
        return f"Error executing code: {str(e)}"


@app.local_entrypoint()
def main():
    # Example LLM-generated code
    code = """
def calculate_fibonacci(n):
    if n <= 1:
        return n
    return calculate_fibonacci(n-1) + calculate_fibonacci(n-2)

result = calculate_fibonacci(10)
    """

    result = run_llm_code.remote(code)
    print(f"Result: {result}")

This example locks down the container to ensure that the code is safe to execute by:

  • Restricting Modal access
  • Using a fresh container for each execution
  • Setting a timeout
  • Blocking network access
  • Catching and handling potential errors

Error Handling

When a restricted Function attempts to access Modal resources, it will raise an AuthError:

@app.function(restrict_modal_access=True)
def restricted_function(q: modal.Queue):
    try:
        # This will fail because the Function is restricted
        return q.get()
    except modal.exception.AuthError as e:
        return f"Access denied: {e}"

The error message will indicate that the operation is not permitted due to restricted Modal access.