Kibotos
Scenarios

Running untrusted code

This page runs code you do not trust in a Workspace with no network access, a small resource limit, no Volume, and a time limit, then reads back a bounded result. The complete program is examples/go/untrusted-code.

Prerequisites

Steps

1. Create a restricted Workspace

Set the limits when you create the Workspace. The expiration time makes ctrl delete it even if your program never comes back:

request := &ctrlv1.CreateWorkspaceRequest{
	Metadata: &ctrlv1.CommandMetadata{CommandId: commandID},
	Spec: &ctrlv1.WorkspaceCreateSpec{
		Template:      &ctrlv1.TemplateSelector{TemplateId: templateID},
		Resources:     &ctrlv1.ResourceRequest{CpuMillis: 500, MemoryBytes: 256 << 20, EphemeralDiskBytes: 1 << 30},
		Volume:        &ctrlv1.WorkspaceVolumeBinding{Mode: ctrlv1.WorkspaceVolumeMode_WORKSPACE_VOLUME_MODE_NONE},
		NetworkPolicy: &ctrlv1.NetworkPolicy{Mode: ctrlv1.NetworkPolicyMode_NETWORK_POLICY_MODE_DENY_ALL},
	},
	Expiration: &ctrlv1.WorkspaceExpirationPolicy{
		Timeout: durationpb.New(10 * time.Minute),
		Action:  ctrlv1.WorkspaceExpirationAction_WORKSPACE_EXPIRATION_ACTION_DELETE,
	},
}

2. Run the code with a time limit

Upload the code, then start it with TimeoutMillis. The process is killed when the limit passes:

stream, err := session.StartProcess(ctx, &ctrlv1.StartRequest{
	Argv:          []string{"/tmp/job.sh"},
	TimeoutMillis: 30_000,
	Mode:          &ctrlv1.StartRequest_Pipe{Pipe: &ctrlv1.ProcessPipeMode{}},
})

3. Check how the process ended

The exit event says whether the code finished or ran out of time. A killed process has no meaningful exit code, so check the reason first:

switch exited.GetTerminationReason() {
case ctrlv1.ProcessTerminationReason_PROCESS_TERMINATION_REASON_EXITED:
	if code := exited.GetExitCode(); code != 0 {
		return fmt.Errorf("the code exited with %d", code)
	}
case ctrlv1.ProcessTerminationReason_PROCESS_TERMINATION_REASON_TIMED_OUT:
	return errors.New("the code ran past its time limit and was killed")
default:
	return fmt.Errorf("the code ended with %s, signal %d", exited.GetTerminationReason(), exited.GetSignal())
}

4. Read a bounded result

Treat the output and the result files as untrusted too. Keep a limited amount of each, and delete the Workspace when you are done:

result, err := io.ReadAll(io.LimitReader(file, maxOutput))

5. Run the program

The built-in script tries to reach the network and writes a result file. Pass -code to run your own script:

go run ./examples/go/untrusted-code -endpoint https://ctrl.example.com:18092 \
  -identity "$HOME/.local/state/kibotos-untrusted-code" -enrollment-key-file untrusted-code.key
created Workspace ws-06gvz1gwahz9f738p47k0d7zpg
output:
wget: bad address 'example.com'
network: unreachable
result: computed 42
deleted Workspace ws-06gvz1gwahz9f738p47k0d7zpg

With the E2B SDK

The SDK requests public internet access by default, so turn it off explicitly. A command that passes its time limit throws an error:

import { Sandbox } from 'e2b';

const sandbox = await Sandbox.create('kibotos-minimal', {
  allowInternetAccess: false,
  timeoutMs: 10 * 60_000,
  lifecycle: { onTimeout: 'kill', autoResume: false },
});
try {
  await sandbox.files.write('/tmp/job.sh', 'echo computed 42 > /tmp/result.txt\n');
  await sandbox.commands.run('sh /tmp/job.sh', { timeoutMs: 30_000 });
  console.log(await sandbox.files.read('/tmp/result.txt'));
} finally {
  await sandbox.kill();
}

The gateway sets the resources of every sandbox, so the SDK cannot lower them for one sandbox. See Resources.

Notes

  • Isolation comes from the MicroVM. The code can run as root inside it, and that does not reach the host or other Workspaces. See Isolation.
  • Give each user or job its own Workspace. A Workspace is one trust boundary, and stopping it keeps its files.
  • A Workspace with no network still answers data-plane calls from your program.

Next steps

On this page