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:
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)
- 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.