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?
We compile CPython, the Python interpreter, to WebAssembly (Wasm). The submitted code stays ordinary Python.
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.
External calls go through functions provided by the Host.
One Host process manages separate Wasm instances.
Load CPython moduleruntime_initruntime_prepareSnapshot+ Module data
+ Python objects
+ NumPy objects
Saved linear memory
Load CPython moduleruntime_initruntime_prepareSnapshot+ Module data
+ Python objects
+ NumPy objects
Saved linear memory
Load CPython moduleruntime_initruntime_prepareSnapshot+ Module data
+ Python objects
+ NumPy objects
Saved linear memory
Load CPython moduleruntime_initruntime_prepareSnapshot+ Module data
+ Python objects
+ NumPy objects
Saved linear memory
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+ Module data
+ Python objects
+ NumPy objects
Saved linear memory
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
At the original call site, use the saved quote if it is for the same ticker and at most five minutes old. Otherwise, request a new quote.
Prepare, Linearize, Materialize: PLM.
a Arguments
t Call time
h Hidden state
An early result may become invalid over time.
Check whether we can still use it.
Here, h includes the market’s current data, which may change before the call.
Start the request early.
Check the inputs and the saved result at the call.
Return the value or error at that call.
a Arguments
t Call time
h Hidden state
An early result may become invalid over time.
Check whether we can still use it.
Here, h includes the market’s current data, which may change before the call.
Start the request early.
Check the inputs and the saved result at the call.
Return the value or error at that 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
Once the inputs are known, Pysolate can start these price reads.
acme = market.recent_price("ACME")Cache hitbeta = market.recent_price("BETA")Waitingportfolio_value = acme + beta
Python uses the saved ACME quote, then waits for 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.
Total time: about 6.3 s → 4.9 s.
The 1.4 s saving is close to one full tool read.
The full program is ready. Each read takes 200 ms.
End-to-end time
The full program is ready. Each read takes 200 ms.
End-to-end time
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
Cannot reuse
Call the tool now
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 goes after the ticker is set and after audit().
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()
A delayed error can allow the write to happen first.
At the callErrorStop
Error raised laterWriteError
Research direction · CRM and email example
customer = tools.crm.get_customer(customer_id)report = build_report(customer)Path("/workspace/report.md").write_text(report)tools.crm.update_customer( customer_id, {"report_status": "ready"})tools.mail.send( customer["email"], subject="Your report", body=report)
Code analysis: order and dependencies
Tool implementations: inputs and conditions
Research direction · Same report, CRM update and email