Run arbitrary code in a sandboxed environment

This example demonstrates how to run arbitrary code in multiple languages in a Modal Sandbox.

Setting up a multi-language environment

Sandboxes allow us to run any kind of code in a safe environment. We’ll use an image with a few different language runtimes to demonstrate this.

import modal

image = modal.Image.debian_slim(python_version="3.11").apt_install(
    "nodejs", "ruby", "php"
)
app = modal.App.lookup("safe-code-execution", create_if_missing=True)

We’ll now create a Sandbox with this image. We’ll also enable output so we can see the image build logs. Note that we don’t pass any commands to the Sandbox, so it will stay alive, waiting for us to send it commands.

with modal.enable_output():
    sandbox = modal.Sandbox.create(app=app, image=image)

print(f"Sandbox ID: {sandbox.object_id}")

Running bash, Python, Node.js, Ruby, and PHP in a Sandbox

We can now use Sandbox.exec to run a few different commands in the Sandbox.

bash_ps = sandbox.exec("echo", "hello from bash")
python_ps = sandbox.exec("python", "-c", "print('hello from python')")
nodejs_ps = sandbox.exec("node", "-e", 'console.log("hello from nodejs")')
ruby_ps = sandbox.exec("ruby", "-e", "puts 'hello from ruby'")
php_ps = sandbox.exec("php", "-r", "echo 'hello from php';")

print(bash_ps.stdout.read(), end="")
print(python_ps.stdout.read(), end="")
print(nodejs_ps.stdout.read(), end="")
print(ruby_ps.stdout.read(), end="")
print(php_ps.stdout.read(), end="")
print()

The output should look something like

hello from bash
hello from python
hello from nodejs
hello from ruby
hello from php

We can use multiple languages in tandem to build complex applications. Let’s demonstrate this by piping data between Python and Node.js using bash. Here we generate some random numbers with Python and sum them with Node.js.

combined_process = sandbox.exec(
    "bash",
    "-c",
    """python -c 'import random; print(\" \".join(str(random.randint(1, 100)) for _ in range(10)))' |
    node -e 'const readline = require(\"readline\");
    const rl = readline.createInterface({input: process.stdin});
    rl.on(\"line\", (line) => {
      const sum = line.split(\" \").map(Number).reduce((a, b) => a + b, 0);
      console.log(`The sum of the random numbers is: ${sum}`);
      rl.close();
    });'""",
)

result = combined_process.stdout.read().strip()
print(result)

For long-running processes, you can use stdout as an iterator to stream the output.

slow_printer = sandbox.exec(
    "ruby",
    "-e",
    """
    10.times do |i|
      puts "Line #{i + 1}: #{Time.now}"
      STDOUT.flush
      sleep(0.5)
    end
    """,
)

for line in slow_printer.stdout:
    print(line, end="")

This should print something like

Line 1: 2024-10-21 15:30:53 +0000
Line 2: 2024-10-21 15:30:54 +0000
...
Line 10: 2024-10-21 15:30:58 +0000

Since Sandboxes are safely separated from the rest of our system, we can run very dangerous code in them!

sandbox.exec("rm", "-rfv", "/", "--no-preserve-root")

This command has deleted the entire filesystem, so we can’t run any more commands. Let’s terminate the Sandbox to clean up after ourselves.

sandbox.terminate()