Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,15 @@ The shim keeps the evaluation function running as a persistent process and commu
| `tcp` | Raw TCP connection. |
| `ws` | WebSocket connection. Experimental — custom dialer configuration is not yet supported. |

Generic WASM and Python Reactor are explicit opt-in execution paths. See
[Execution paths](docs/execution-paths.md) for their environment contracts,
lifecycle behavior, and compatibility boundaries.

To try Python Reactor without assembling requests by hand, follow the
[`safe-eval-python` three-command quick start](examples/safe-eval-python/README.md#start-here-first-successful-evaluation).
It includes runnable base, NumPy, and SymPy fixtures plus both passing and
failing student-code examples.

The shim injects the following environment variables into the evaluation function process so it can identify the transport it should listen on:

| Variable | Value |
Expand Down Expand Up @@ -246,16 +255,24 @@ For example, a Wolfram Language evaluation function in `evaluation.wl` would be
wolframscript -file evaluation.wl /tmp/shimmy/abc/request-data-123 /tmp/shimmy/abc/response-data-456
```

### Sandboxed Execution (Linux only, experimental)
### Sandboxed Execution (Linux host/container only, experimental)

Shimmy can wrap each worker process in an [nsjail](https://github.com/google/nsjail) sandbox to safely execute arbitrary, untrusted code. The sandbox provides:
On supported Linux hosts, Shimmy can wrap each worker process in an [nsjail](https://github.com/google/nsjail) sandbox to execute untrusted code with an additional OS boundary. The sandbox provides:

- **Filesystem confinement** — the worker can only access explicitly bind-mounted paths
- **Resource limits** — CPU time, memory, and file descriptor caps
- **Network isolation** — optional; disables all outbound connections
- **Unprivileged UID** — worker runs as `nobody` (uid 65534) inside the jail

Sandboxing requires Linux and the `nsjail` binary. The Docker image built from the project's `Dockerfile` includes nsjail at `/usr/sbin/nsjail`. On the host, install it with `sudo apt install nsjail` (Ubuntu 22.04+) or build from source.
Sandboxing requires Linux, the `nsjail` binary, and permission to create the
required namespaces/capabilities. The Docker image built from the project's
`Dockerfile` includes nsjail at `/usr/sbin/nsjail`. On the host, install it with
`sudo apt install nsjail` (Ubuntu 22.04+) or build from source.

> **AWS Lambda:** Lambda does not grant the namespace/capability controls needed
> to enable this nsjail path. Shipping the binary in a Lambda container image
> does not make it an available security boundary. Use the in-process WASM
> execution profiles for Lambda-compatible isolation.

Enable sandboxing with `--sandbox` and configure it with the flags below:

Expand Down
24 changes: 20 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"strings"
"time"

"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -47,18 +48,17 @@ functions on arbitrary, serverless platforms.`
&cli.StringFlag{
Name: "interface",
Aliases: []string{"i"},
Usage: "the interface to use for worker process communication. Options: rpc, file.",
Usage: "the interface to use for worker communication. Options: rpc, file, wasm.",
Value: "rpc",
Category: "function",
EnvVars: []string{"FUNCTION_INTERFACE"},
},
&cli.StringFlag{
Name: "command",
Aliases: []string{"c"},
Usage: "the command to invoke to start the worker process.",
Usage: "the command to invoke to start the worker process, or the WASM module path when --interface=wasm.",
Category: "function",
Comment thread
bkmashiro marked this conversation as resolved.
EnvVars: []string{"FUNCTION_COMMAND"},
Required: true,
},
&cli.StringFlag{
Name: "cwd",
Expand Down Expand Up @@ -360,6 +360,22 @@ func parseRootConfig(ctx *cli.Context) (config.Config, error) {
if err != nil {
return config.Config{}, err
}
if err := validateRootConfig(cfg, os.Getenv("FUNCTION_WASM_MODULE")); err != nil {
return config.Config{}, err
}

return cfg, nil
}

return cfg, err
func validateRootConfig(cfg config.Config, wasmModule string) error {
if strings.TrimSpace(cfg.Runtime.Supervisor.StartParams.Cmd) != "" {
return nil
}
if cfg.Runtime.Supervisor.IO.Interface == "wasm" {
if strings.TrimSpace(wasmModule) != "" {
return nil
}
return fmt.Errorf("wasm interface requires --command or FUNCTION_WASM_MODULE")
}
return fmt.Errorf("%s interface requires --command", cfg.Runtime.Supervisor.IO.Interface)
}
37 changes: 37 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cmd

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/lambda-feedback/shimmy/config"
"github.com/lambda-feedback/shimmy/internal/execution/supervisor"
)

func TestValidateRootConfigRequiresCommandForProcessInterfaces(t *testing.T) {
for _, iface := range []supervisor.IOInterface{supervisor.RpcIO, supervisor.FileIO} {
t.Run(string(iface), func(t *testing.T) {
var cfg config.Config
cfg.Runtime.Supervisor.IO.Interface = iface
err := validateRootConfig(cfg, "")
require.Error(t, err)
assert.Contains(t, err.Error(), "--command")
})
}
}

func TestValidateRootConfigAcceptsWasmModuleOverride(t *testing.T) {
var cfg config.Config
cfg.Runtime.Supervisor.IO.Interface = supervisor.WasmIO
require.NoError(t, validateRootConfig(cfg, "/opt/evaluator.wasm"))
}

func TestValidateRootConfigRequiresWasmModulePath(t *testing.T) {
var cfg config.Config
cfg.Runtime.Supervisor.IO.Interface = supervisor.WasmIO
err := validateRootConfig(cfg, "")
require.Error(t, err)
assert.Contains(t, err.Error(), "FUNCTION_WASM_MODULE")
}
74 changes: 74 additions & 0 deletions cmd/shimmy-artifact-check/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// shimmy-artifact-check validates caller-produced WebAssembly artifacts without
// starting Shimmy's production request path.
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"

"github.com/lambda-feedback/shimmy/internal/execution/wasm"
)

func main() {
os.Exit(run(os.Args[1:]))
}

func run(args []string) int {
flags := flag.NewFlagSet("shimmy-artifact-check", flag.ContinueOnError)
flags.SetOutput(os.Stderr)
profile := flags.String("profile", "generic", "runtime ABI: generic or python-reactor")
module := flags.String("module", "", "path to a prebuilt WebAssembly module")
manifest := flags.String("manifest", "", "Python Reactor manifest path")
buildCommand := flags.String("build-command", "", "explicit producer command to run before validation")
buildDir := flags.String("build-dir", ".", "working directory for --build-command")
jsonOutput := flags.Bool("json", false, "emit a JSON report")
if err := flags.Parse(args); err != nil {
return 2
}
if flags.NArg() != 0 {
fmt.Fprintf(os.Stderr, "unexpected arguments: %v\n", flags.Args())
return 2
}

if *buildCommand != "" {
command := exec.Command("/bin/sh", "-c", *buildCommand)
command.Dir = *buildDir
command.Stdout = os.Stdout
command.Stderr = os.Stderr
if err := command.Run(); err != nil {
fmt.Fprintf(os.Stderr, "artifact build failed: %v\n", err)
return 1
}
}

report, err := wasm.CheckArtifact(context.Background(), wasm.ArtifactCheckOptions{
Profile: *profile,
ModulePath: *module,
ManifestPath: *manifest,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if *jsonOutput {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(report); err != nil {
fmt.Fprintf(os.Stderr, "encode report: %v\n", err)
return 1
}
return 0
}

fmt.Printf("OK %s artifact: %s\n", report.Profile, report.Module)
fmt.Printf("exports: %v\n", report.Exports)
fmt.Printf("imports: %v\n", report.Imports)
for _, warning := range report.Warnings {
fmt.Printf("WARNING: %s\n", warning)
}
return 0
}
118 changes: 118 additions & 0 deletions docs/execution-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# WebAssembly execution paths

Shimmy keeps the existing `rpc` and `file` process interfaces and adds two
explicit, opt-in WebAssembly paths. Selection is configuration-driven; Shimmy
does not inspect source files or silently retry a request under another backend.

## Generic WebAssembly

```bash
FUNCTION_INTERFACE=wasm
FUNCTION_WASM_PROFILE=generic
FUNCTION_WASM_MODULE=/opt/evaluator/evaluator.wasm
```

The module runs in-process under wazero and exports `memory`, `alloc`, and
`dispatch`. Shimmy copies each request into guest linear memory, copies the
response out, and restores the prepared memory before reusing the instance.

Memory reset uses one portable implementation: a full copy of linear memory.
There is no snapshot-strategy selector in this path. If a request grows linear
memory, the instance is discarded because WebAssembly memory cannot shrink back
to the captured size.

The generic path has no host filesystem access unless paths are explicitly
allowed with `FUNCTION_WASM_ALLOWED_PATHS`. Environment variables are similarly
allowlisted with `FUNCTION_WASM_ALLOWED_ENV`.

## Python Reactor

```bash
FUNCTION_INTERFACE=wasm
FUNCTION_WASM_PROFILE=python-reactor
FUNCTION_WASM_MODULE=/opt/runtime/python-reactor.wasm
FUNCTION_WASM_MANIFEST=/opt/runtime/manifest.json
FUNCTION_WASM_PYTHON_SCRIPT=/opt/evaluator/evaluator.py
FUNCTION_WASM_PYTHON_LIFECYCLE=snapshot
```

The prepared trusted script owns `dispatch(method, payload)`. Shimmy verifies
the following before serving requests:

- artifact SHA-256 against the manifest;
- Reactor ABI name and version;
- required imports, exports, and function signatures; and
- manifest-declared `python_modules` against the artifact capability section.

This proves that the selected artifact and manifest are internally consistent.
Artifact authenticity, trusted Producer commit policy, signatures, and release
provenance remain deployment-system responsibilities.

### Lifecycle choices

| Value | Behavior |
|---|---|
| `snapshot` | Prepare once per slot and restore the full linear-memory copy after each successful request. Failed or timed-out slots are discarded and replenished asynchronously. |
| `single-use` | Prepare candidates ahead of time, serve each candidate once, then replace it. |
| `fresh` | Instantiate and prepare a new module for every request. |

`snapshot` is the default and the only lifecycle that restores memory. Its reset
implementation is always full-memory copy; there is no snapshot-strategy
configuration. `single-use` and `fresh` are lifecycle alternatives, not hidden
fallbacks. Shimmy never changes lifecycle after a request fails.

Python Reactor does not expose host paths. Leave
`FUNCTION_WASM_ALLOWED_PATHS` unset. Runtime modules are selected by the
manifest-validated artifact profile, for example `base`, `numpy-core`, or
`sympy`.

### Linux HTTP verification

Run the HTTP startup and request-flow check against a real Producer artifact and
its exact manifest:

```bash
SHIMMY_PYTHON_REACTOR_WASM=/opt/runtime/python-reactor.wasm \
SHIMMY_PYTHON_REACTOR_MANIFEST=/opt/runtime/manifest.json \
scripts/e2e-python-reactor.sh
```

The check starts Shimmy, sends two `eval` requests and one `preview` request,
and verifies prepared-state restoration between requests.

## Safe Python evaluator example

[`examples/safe-eval-python`](../examples/safe-eval-python/README.md) is a
backend-level Python Reactor example for student Python in `demo`, `io_test`,
`unit_test`, and `preview` modes. It uses:

- wazero's WebAssembly capability boundary;
- request deadlines;
- artifact, ABI, and manifest-capability validation;
- full-copy memory reset and failed-slot replacement; and
- evaluator limits for code, input, tests, and output.

It does not depend on nsjail, privileged Lambda configuration, Node, Docker, or
runtime package installation. AST checks provide early feedback and defense in
depth; they are not a containment boundary.

```bash
SHIMMY_PYTHON_REACTOR_WASM=/path/to/base.wasm \
SHIMMY_PYTHON_REACTOR_MANIFEST=/path/to/base.manifest.json \
scripts/e2e-safe-eval-python.sh
```

For the guided base, NumPy, and SymPy examples, follow the
[quick start](../examples/safe-eval-python/README.md#start-here-first-successful-evaluation).

## Security boundary

WebAssembly isolation, request deadlines, state reset, and evaluator-level
limits do not form a complete operating-system sandbox. Deployment policy still
owns process memory, aggregate concurrency, authentication, request-size limits,
logging, artifact provenance, and network exposure.

AWS Lambda cannot grant the namespaces or capabilities required to use nsjail
as a security boundary. On supported Linux hosts or containers, an external OS
sandbox may be added as a separate deployment layer; Shimmy does not claim that
boundary for Lambda.
Loading