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 function 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)
  1. Minimize the App source that’s included in the container

A restricted Modal Function will have read access to its source files in the container, so you’ll want to avoid including anything that would be harmful if exfiltrated by the untrusted process.

If deploying an App from within a larger package, the entire package source may be automatically included by default. A best practice would be to make the untrusted Function part of a standalone App that includes the minimum necessary files to run:

restricted_app = modal.App("restricted-app", include_source=False)

image = (
    modal.Image.debian_slim()
    .add_local_file("restricted_executor.py", "/root/restricted_executor.py")
)

@restricted_app.function(
    restrict_modal_access=True,
    block_network=True,
    max_inputs=1,
)
def 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.