A Semantics-Aware Sandbox
for Efficient Isolated Execution
A Semantics-Aware Sandbox
for Efficient Isolated Execution
Yuzhe Shi
MSc Individual Project · Imperial College London
Imagine a data analysis class. Students need a platform to run their code.
We start with a Python server. It executes submissions in the same interpreter.
exec(submission)import numpy as npscores = np.loadtxt("scores.csv")[1., 2., 3.]result = scores.mean()2.0
import numpy as np def missing_file(path): raise FileNotFoundError(path) np.loadtxt = missing_file
import numpy as np def missing_file(path): raise FileNotFoundError(path) np.loadtxt = missing_file
import numpy as npscores = np.loadtxt("scores.csv")FileNotFoundErrorresult = scores.mean()
import numpy as npscores = np.loadtxt("scores.csv")FileNotFoundErrorresult = scores.mean()
import numpy as npscores = np.loadtxt("scores.csv")FileNotFoundErrorresult = scores.mean()
How can every run start clean?
Can we reuse setup and keep runs separate?
Can we reuse setup and keep runs separate?
To control interpreter state, we compile CPython to WebAssembly (Wasm), a portable instruction format.
A runtime loads the module and executes its instructions.
The runtime checks accesses to linear memory.
The Host application connects the functions the module can call.
The Host can capture and remap memory for snapshots, COW and reset.
Calls cross explicit imports, so the Host can inspect, log and schedule them.
One Host process manages separate Wasm instances.
Load CPython moduleruntime_initruntime_prepareSnapshot+ Wasm module
+ Python runtime
+ NumPy modules
Snapshot
Load CPython moduleruntime_initruntime_prepareSnapshot+ Wasm module
+ Python runtime
+ NumPy modules
Snapshot
Load CPython moduleruntime_initruntime_prepareSnapshot+ Wasm module
+ Python runtime
+ NumPy modules
Snapshot
Load CPython moduleruntime_initruntime_prepareSnapshot+ Wasm module
+ Python runtime
+ NumPy modules
Snapshot
The Host reads linear memory as []byte. After setup returns, we can save these bytes and restore them into a fresh instance.
Load CPython moduleruntime_initruntime_prepareSnapshot+ Wasm module
+ Python runtime
+ NumPy modules
Snapshot
Copy the whole image for every run?
With 28 GiB memory available
Each run keeps its writes private.
Clean pages stay shared across runs.
Drop private changes and reuse the prepared state.
Many short agent programs finish their calculations quickly, then wait for I/O.
values = np.array([12, 18, 24])result = values.mean()
acme = market.recent_price("ACME")beta = market.recent_price("BETA")portfolio_value = acme + beta
item = json.loads(payload)name = item["name"]
text = "\n".join(notes)tools.send_email(text)
One share of ACME and one share of BETA.
acme = market.recent_price("ACME")beta = market.recent_price("BETA")portfolio_value = acme + beta
Both reads use the Tool ABI, so the Host can schedule them.
acme = market.recent_price("ACME")beta = market.recent_price("BETA")portfolio_value = acme + beta
Each call waits for its result before Python continues.
acme = market.recent_price("ACME")beta = market.recent_price("BETA")portfolio_value = acme + beta
Both tickers are known. Why wait?
q_acme = prepare(market.recent_price, "ACME")q_beta = prepare(market.recent_price, "BETA") acme = materialize(q_acme)beta = materialize(q_beta)portfolio_value = acme + beta
Our ABI identifies the tool and its arguments.
Prepare is non-blocking. It starts the request and returns a handle.
q_acme = prepare(market.recent_price, "ACME")q_beta = prepare(market.recent_price, "BETA") acme = materialize(q_acme)beta = materialize(q_beta)portfolio_value = acme + beta
The first materialize blocks until ACME is ready. Python then reaches the second materialize. BETA is already ready, so it returns immediately.
q_acme = prepare(market.recent_price, "ACME")q_beta = prepare(market.recent_price, "BETA") acme = materialize(q_acme)beta = materialize(q_beta)portfolio_value = acme + beta
Will the quote still be valid at the call?
q_acme = prepare(market.recent_price, "ACME")q_beta = prepare(market.recent_price, "BETA")heavy_work()acme = materialize(q_acme)beta = materialize(q_beta)portfolio_value = acme + beta
Suppose independent work runs before the price call. The quote arrives while Python is still busy.
q_acme = prepare(market.recent_price, "ACME")q_beta = prepare(market.recent_price, "BETA")heavy_work()acme = materialize(q_acme)beta = materialize(q_beta)portfolio_value = acme + beta
The tool allows quotes up to five minutes old. By the time this call runs, our saved quote is too old.
q_acme = prepare(market.recent_price, "ACME")q_beta = prepare(market.recent_price, "BETA")heavy_work()acme = materialize(linearize(q_acme))beta = materialize(linearize(q_beta))portfolio_value = acme + beta
Linearize checks reuse at the original call site. It accepts a valid request or starts a fresh one.
Prepare, Linearize, Materialize: PLM.
a Arguments
t Call time
h Hidden state
Optimized call ≡ original call at the call site
Both calls must satisfy the same rule for values, errors and effects.
Here, h includes the market’s current data, which may change before the call.
Start early when the Host rule allows it.
Recheck arguments, time and relevant state.
Deliver the value or error at the original call.
a Arguments
t Call time
h Hidden state
Optimized call ≡ original call at the call site
Both calls must satisfy the same rule for values, errors and effects.
Here, h includes the market’s current data, which may change before the call.
Start early when the Host rule allows it.
Recheck arguments, time and relevant state.
Deliver the value or error at the original call.
listing = tools.listing("ACME")if listing["market_open"]: ticker = listing["ticker"] label = "Current price" price = market.recent_price(ticker)
listing = tools.listing("ACME")if listing["market_open"]: ticker = listing["ticker"] q = prepare(market.recent_price, ticker) label = "Current price" price = materialize(linearize(q))
acme = market.recent_price("ACME")Readybeta = market.recent_price("BETA")In progressportfolio_value = acme + beta
… more code is arriving
As each complete call arrives, the source pass starts its tool request.
acme = market.recent_price("ACME")Cache hitbeta = market.recent_price("BETA")Waitingportfolio_value = acme + beta
Python starts for the first time. ACME hits its cache. Python waits at BETA.
acme = market.recent_price("ACME")Cache hitbeta = market.recent_price("BETA")Returnedportfolio_value = acme + beta
BETA returns from the same prepared request. Python continues with the sum.
One 1.5 s tool read overlapped a 1.4 s source stream.
End-to-end time fell by 22.3% compared with serial execution.
The 1.4 s saving is close to one full tool read.
Within a complete program, each read takes 200 ms.
Within a complete program, each read takes 200 ms.
A prepared CPython image with private pages on write.
Isolated runs, low memory use and fast reset.
Shimmy, versus full-copy reset
A source pass and Host request table implement Prepare, Linearize and Materialize.
Tool work overlaps execution and code generation.
Source-stream benchmark, versus serial
Python heap
Imported module objects
Prepared data
An instance of the same module
Fresh globals and tables
Host resources and workspace
memfd / MAP_PRIVATE_initializeMADV_DONTNEEDGuest pointers are offsets. The Host mapping can move.
alloc(n) → request_offsetwrite JSON bytes at request_offsetexecute(request_offset, n)response_offset → [u32 length][JSON]dealloc(request_offset)
Offsets are 32-bit positions in linear memory. The response frame begins with a little-endian 4-byte payload length.
Pending
Wait at the call
Ready
Check and return
Invalid
Take the original call path
ticker = "ACME"audit()label = "Latest quote"price = market.recent_price(ticker)result = [label, price]
ticker = "ACME"audit()q = prepare(market.recent_price, ticker)label = "Latest quote"price = materialize(linearize(q))result = [label, price]
Prepare follows its inputs and movement barriers. It stays inside the controlling branch.
PyImport_AppendInittab( "numpy.core._multiarray_umath", PyInit__multiarray_umath);
open("out.txt", "w").write("done")if (
A late syntax error stops Python before the file write.
price = market.recent_price("ACME")write_receipt()
Moving delivery later can change what happens first.
At the callErrorStop
Delayed deliveryWriteError