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:
Feature | Restricted Function | Sandbox |
---|---|---|
State | Stateless | Stateful |
Interface | Function-like | Container-like |
Setup | Simple decorator | Requires explicit creation/termination |
Use case | Quick, isolated code execution | Interactive development, long-running sessions |
Best Practices
When running untrusted code, consider these additional security measures:
- 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)
- 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)
- 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.