This is CS50 Sandbox.

CS50 Sandbox is a sandbox in which untrusted code can be executed. It supports these operations:

Installation

Amazon EC2

  1. Spawn an c1.medium instance of AMI ami-08d97e61 (which is 32-bit Fedora 17), per http://fedoraproject.org/wiki/Cloud_images.
  2. sudo su -
  3. yum -y install http://mirror.cs50.net/appliance50/17/i386/RPMS/library50-c-4-1.i386.rpm
  4. yum -y install http://mirror.cs50.net/sandbox50/sandbox50-0-22.i386.rpm
  5. git clone git@github.com:cs50/run50.git /opt/sandbox50/public/run50
  6. Visit http://w.x.y.z:8080/run50/ to use CS50 Run, where w.x.y.z is instance's IP address.

CS50 Appliance

  1. Install CS50 Appliance 17.
  2. sudo yum -y update appliance50
  3. reboot
  4. sudo su
  5. yum -y install http://mirror.cs50.net/sandbox50/sandbox50-0-22.i386.rpm
  6. git clone git@github.com:cs50/run50.git /opt/sandbox50/public/run50
  7. Visit http://w.x.y.z:8080/run50/ to use CS50 Run, where w.x.y.z is appliance's IP address.

*To implement a series of checks with an identifier of foo, create a text file called /usr/share/sandbox50/checks/foo.js or /usr/share/sandbox50/checks/foo/index.js.
See /usr/share/sandbox50/checks/ for examples. See implementation details for documentation. Use http://w.x.y.z:8080/examples/check.html to experiment with CS50 Check.

Usage

For actual examples, see public/examples/.

check

To run a series of checks against a previously uploaded directory of files, POST to /check with a Content-Type of application/json a JSON object of the form

{
    "checks": String,
    "sandbox": { "homedir": String }
}

where checks is the unique identifier for those checks and sandbox defines a "sandbox" to use for the command's execution, where homedir is the unique identifier for a previously uploaded directory of files, a copy of which will be mounted as $HOME for each of the checks in the series.

Upon successful execution, the server will return a JSON object of the form

{
    "results": Object
}

where results is an object, each of whose keys is the unique identifier for a check in the series, the value of which is an object of the form

{
    "dependencies": [String, ...],
    "description": String,
    "result": Object,
    "script": [Object, ...]
}

where dependencies is an array, each of whose elements is the unique identifier for another check on which the check depends, description is a human-friendly description of the check, result is either

and script is an array of objects, each of the form

{
    "actual": { type: String, value: String },
    "expected": { type: String, value: String }
}

where expected describes what was expected, and actual describes what actually occurred, if anything, up until the point when the check passed or failed.

For instance, this response indicates that two checks passed:

{
    "results": {
        "compiles": {
            "dependencies": [],
            "description": "Does file.c compile?",
            result: true
        },
        "runs": {
            "dependencies": [
                "compiles"
            ],
            "description": "Does a.out run?",
            "result": true
        }
    }
}

By contrast, this response indicates that one check passed whereas another check failed (because of an unexpected exit code):

{
    "results": {
        "compiles": {
            "dependencies": [],
            "description": "Does file.c compile?",
            "result": true
        },
        "runs": {
            "dependencies": [
                "compiles"
            ],
            "description": "Does a.out exit with 0?",
            "result": false,
            "script": [
                {
                    "actual": {
                        "type": "exit",
                        "value": 1
                    },
                    "expected": {
                        "type": "exit",
                        "value": 0
                    }
                }
            ]
        }
    }
}

Similarly does this response indicate that one check passed whereas another check failed (because of unexpected standard output):

{
    "results": {
        "compiles": {
            "dependencies": [],
            "description": "Does file.c compile?",
            "result": true
        },
        "runs": {
            "dependencies": [
                "compiles"
            ],
            "description": "Does a.out print \"foo\"?",
            "result": false,
            "script": [
                {
                    "actual": {
                        "type": "stdout",
                        "value": "bar"
                    },
                    "expected": {
                        "type": "stdout",
                        "value": "foo"
                    }
                }
            ]
        }
    }
}

And this response indicates that two checks failed, one of which wasn't even executed because of its dependency on the other:

{
    "results": {
        "compiles": {
            "dependencies": [],
            "description": "Does file.c compile?",
            "result": false,
            "script": [
                {
                    "actual": {
                        "type": "exit",
                        "value": 1
                    },
                    "expected": {
                        "type": "exit",
                        "value": 0
                    }
                }
            ]
        },
        "runs": {
            "dependencies": [
                "compiles"
            ],
            "description": "Does a.out run?",
            "result": null,
            "script": []
        }
    }
}

Upon any error that prevents execution of checks altogether, the server will instead return a JSON object of the form

{
    "error": Object
}

where error is an error object, with an HTTP status code of 400.

Upon any error that prevents execution of a particular check, the server will return a key of error, whose value is an explanatory string, instead of a key of result for that check.

run

To run commands inside of CS50 Sandbox, you may use HTTP POST or socket.io.

POST

To run a command inside of CS50 Sandbox via POST, POST to /run with a Content-Type of application/json a JSON object of the form

{
    "cmd": String,
    "sandbox": { "homedir": String }
}

where cmd is the command to be executed (including any command-line arguments) and sandbox defines a "sandbox" to use for the command's execution, where homedir is the unique identifier for a previously uploaded directory of files, a copy of which will be mounted as $HOME.

Upon successful execution, the server will return a JSON object of the form

{
    "code": Number,
    "sandbox": String,
    "script": String,
    "stderr": String,
    "stdout": String
}

where code is the command's exit code, sandbox is a unique identifier for the sandbox in which the command was executed (in which subsequent commands could also be run), script is the command's terminal output (with standard error and standard output interwoven), stderr is the command's standard error, and stdout is the command's standard output, with an HTTP status code of 200.

To run a subsequent command inside of the same sandbox, POST to run with a Content-Type of application/json a JSON object of the form

{
    "cmd": String,
    "sandbox": String
}

where cmd is the subsequent command to be executed (including any command-line arguments) and sandbox is the unique identifier for the sandbox in which the previous command was executed.

Upon any error, the server will instead return a JSON object of the form

{
    "error": Object
}

where error is an error object, with an HTTP status code of 400.

socket.io

To run a command inside of CS50 Sandbox via socket.io, open a connection as follows

var socket = io.connect('https://run.cs50.net', { 'force new connection': true });

where run.cs50.net is CS50 Sandbox's hostname, and then emit a run event, passing in a JSON object of the form

{
    "cmd": String,
    "sandbox": { "homedir": String }
}

where cmd is the command to be executed (including any command-line arguments) and sandbox defines a "sandbox" to use for the command's execution, where homedir is the unique identifier for a previously uploaded directory of files, a copy of which will be mounted as $HOME.

If the executing command is waiting for standard input, the server will emit a stdin event with no payload.

To send standard input to the executing command, emit a stdin event whose payload is a String. To send EOF, emit an EOF event with no payload. To send SIGINT, emit a SIGINT event with no payload.

If the command's execution produces standard error, it will be emitted by a stderr event whose payload is a String. If the command's execution produces standard output, it will be emitted by a stdout event whose payload is a String.

Upon success execution, the server will emit an exit event whose payload is a JSON object of the form

{
    "code": Number,
    "sandbox": String,
    "script": script,
    "stderr": String,
    "stdout": String
}

where code is the command's exit code, sandbox is a unique identifier for the sandbox in which the previous command was run (in which subsequent commands could also be run), script is the command's terminal I/O interwoven (with standard error and standard output interwoven), stderr is the command's standard error, and stdout is the command's standard output. (Even though stderr and stdout are also emitted by separate events, they are included in the server's response for convenience, in case the command's execution produced standard error or standard output before erring altogether.)

To run a subsequent command inside of CS50 Sandbox via socket.io, open a new connection as follows

var socket = io.connect('https://run.cs50.net', { 'force new connection': true });

where run.cs50.net is CS50 Sandbox's hostname, and then emit a run event, passing in a JSON object of the form

{
    "cmd": String,
    "sandbox": String
}

where cmd is the subsequent command to be executed (including any command-line arguments) and sandbox is the unique identifier for the sandbox in which the previous command was executed.

Upon any error, the server will instead emit an error event whose payload is a JSON object of the form

{
    "error": Object
}

where error is an error object.

save

To trigger a browser to save an HTML textarea (or other String) locally as a file, POST to /save with a Content-Type of application/x-www-form-urlencoded a single key/value pair, where the key is the file's name and the value is the file's contents.

The server's response will be of type application/octet-stream, thereby triggering a file to be saved locally.

Upon error, the server will instead return a JSON object of the form

{
    "error": Object
}

where error is an error object, with an HTTP status code of 400.

upload

To upload files to CS50 Sandbox, POST to /upload with a Content-Type of application/x-www-form-urlencoded or multipart/form-data. Each key/value pair POSTed will be saved server-side as a file, whereby the file's name will be the key and the file's contents will be the value, the latter of is treated as a file in the case of multipart/form-data (as though it were an HTML input for which type="file") or a String in the case of application/x-www-form-urlencoded (as though it were an HTML textarea or an HTML input for which type="text").

{
    "id": String,
    "uploads": [String, ...]
}

where uploads is an array of filenames saved and id is a unique identifer for those uploads' parent directory, with an HTTP status code of 200.

Upon error, the server will instead return a JSON object of the form

{
    "error": Object
}

where error is an error object, with an HTTP status code of 400.

Errors

Upon any error, the server will either return (if using Ajax) a JSON object of the form

{
    "error": { "code": String, "message": String }
}

or emit (if using socket.io) an error event whose payload is a JSON object of the same form, where code is any of

and message, if present, is an explanatory string.

Hosting

AWS

These instructions arbitrarily assume an availability zone of **us-east-1*.*

  1. Visit https://console.aws.amazon.com/vpc/home?region=us-east-1#s=Home.
  2. Click Create another VPC or Get started creating a VPC.
  3. Select VPC with Public and Private Subnets, then click Continue.
  4. Specify the below, then click Create VPC.
    • One VPC with an Internet Gateway
      • IP CIDR block: 10.0.0.0/16
    • 2 Subnets
      • Public Subnet: 10.0.0.0/24
      • Availability Zone: us-east-1a
      • Private Subnet: 10.0.1.0/24
      • Availability Zone: us-east-1a
    • One NAT Instance with an Elastic IP Address
      • Instance Type: m1.small
      • Key Pair Name: up to you
    • Hardware Tenancy
      • Tenancy: Default
  5. Once "Your VPC has been successfully created", click Close.
  6. Visit https://console.aws.amazon.com/vpc/home?region=us-east-1#s=vpcs and take note of the VPC's VPC ID.
  7. Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=Instances, find the m1.small instance that was launched in the VPC's public subnet, and assign it a Name of run-us-east-1a.
  8. Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=Home.
  9. Click Launch Instance.
  10. Select Classic Wizard, then click Continue.
  11. Select Community AMIs, then search for ami-08d97e61 (per http://fedoraproject.org/wiki/Cloud_images), then click Select.
  12. Specify the below, then click Continue.
    • Number of Instances: 2
    • Instance Type: High-CPU Medium (c1.medium, 1.7 GB)
    • Launch into: VPC
    • Subnet: 10.0.1.0/24 (*i.e.*, the private subnet)
  13. Specify the below, then click Continue.
    • Kernel ID: Use Default
    • RAM Disk ID: Use Default
    • Monitoring: unchecked
    • User Data: blank
    • Termination Protection: checked
    • Shutdown Behavior: Stop
    • IAM Role: None
    • Tenancy: Default
    • Number of Network Interfaces: 1 with defaults
  14. Specify the below, then click Continue.
    • Name: sandbox50
  15. Select a key pair, then click Continue.
  16. Select or create a security group that allows inbound TCP 22 (SSH), TCP 80 (HTTP), TCP 443 (HTTPS), and TCP 8080, then click Continue.
  17. Click Launch.
  18. Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=Instances, find the m1.small instance that was launched in the VPC's public subnet, and change its security group to run (as by right-clicking it).
  19. Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=LoadBalancers.
  20. Click Create a Load Balancer.
  21. Specify the below, then click Continue:
    • Load Balancer Name: sandbox50-us-east-1
    • Create LB inside: 10.0.0.0/16 (*i.e.*, VPC)
    • Listener Configuration:
      • HTTP | 80 | HTTP | 8080
  22. Specify the below, then click Continue:
    • Ping Protocol: HTTP
    • Ping Port: 8080
    • Ping Path: /status
    • Response Timeout: 5 Seconds
    • Health Check Interval: 0.1 Minutes
    • Unhealthy Threshold: 2
    • Healthy Threshold: 2
  23. Add 10.0.0.0/24 (*i.e.*, public subnet) to Selected Subnets, then click Continue.
  24. Select same security group as before, then click Continue.
  25. Check both sandbox50 instances, then click Continue.
  26. Click Create.
  27. Click Close.
  28. Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=LoadBalancers.
  29. Take note of the DNS Name of the load balancer and create a CNAME (*e.g.*, run.cs50.net) that resolves to it.

SSH to each sandbox50 instance, disable iptables.

Implementation Details

Checks

By default, checks live in /usr/share/sandbox50/checks. If the unique identifier for a series of checks is foo, then CS50 Sandbox will look for those checks in /usr/share/sandbox50/checks/foo.js or, if not found there, in /usr/share/sandbox50/checks/foo/index.js. Forward slashes are treated as path separators, so if the unique identifier for a series of checks is foo/bar, CS50 Sandbox will look for those checks in /usr/share/sandbox50/checks/foo/bar.js or, if not found there, in /usr/share/sandbox50/checks/foo/bar/ind .

A series of checks, meanwhile, is implemented as a node module in which module.exports is assigned an Object, each of whose keys is a unique identifier for a check. The value of each of those keys is either a Function or an Array, the last of whose elements is a Function, the rest of whose elements are the unique identifiers for other checks on which the check depends.

Each check must return a chain by calling a function called check (whose only argument is a String that describes the check), followed by one or more "links."

For instance, this module contains a series of two checks, the latter of which depends on the former (in which case it will only be executed if the former passes):

module.exports = {

    compiles: function(check) {
        return check('Does src.c compile?')
        .run('clang src.c -lcs50')
        .exit(0);
    },

    runs: ['compiles', function(check) {
        return check('Does a.out run properly?')
        .run('clang src.c -lcs50')
        .exit(0)
        .run('./a.out')
        .stdout('hello, world\n')
        .exit(0);
    }]

};

Note that, even though runs depends on compiles, it must still compile src.c itself, since each check is run in isolation.

Supported links include:

Files

/etc/php.ini

short_open_tag = On
...
date.timezone = America/New_York    

Troubleshooting

Error: EACCES, Permission denied

/opt/sandbox50/node_modules/forever/node_modules/daemon/lib/daemon.js:34
    : binding.start(stdout);
          ^
Error: EACCES, Permission denied
    at Object.start (/opt/sandbox50/node_modules/forever/node_modules/daemon/lib/daemon.js:34:15)
    at Object.startDaemon (/opt/sandbox50/node_modules/forever/lib/forever.js:381:16)
    at /opt/sandbox50/node_modules/forever/lib/forever/cli.js:229:27
    at /opt/sandbox50/node_modules/forever/lib/forever/cli.js:134:5
    at Object.oncomplete (/opt/sandbox50/node_modules/forever/lib/forever.js:342:24)

If seeing this error, odds are you've run index.js as someone other than sandbox50. To resolve:

rm -r /var/sandbox50/*
rm -r /var/log/sandbox50/*

References

License

http://creativecommons.org/licenses/by-nc-sa/3.0/