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 keep runs independent and use fewer resources?
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.
What happens when Mallory writes?
Dots: medians. Bars: 25th–75th percentiles.
PSS: the Host process with all instances open.
What happens while a task waits for a tool?
acme = market.recent_price("ACME")beta = market.recent_price("BETA")portfolio_value = acme + beta
External request
External request
Local calculation
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")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
Suppose independent work runs before the price call. The quote arrives while Python is still busy.
Can we still use the quote 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
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.
acme = market.recent_price("ACME")beta = market.recent_price("BETA")portfolio_value = acme + beta
q_acme = prepare( market.recent_price, "ACME")q_beta = prepare( market.recent_price, "BETA")acme = materialize(linearize(q_acme))beta = materialize(linearize(q_beta))portfolio_value = acme + beta
acme = market.recent_price("ACME")beta = market.recent_price("BETA")portfolio_value = acme + beta
q_acme = prepare( market.recent_price, "ACME")q_beta = prepare( market.recent_price, "BETA")acme = materialize(linearize(q_acme))beta = materialize(linearize(q_beta))portfolio_value = acme + beta
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.
Run the program
Rewrite the calls
Then run the program
Does the optimization save time overall?
The full program is ready. Each read takes 200 ms.
End-to-end time
Dots: medians. Bars: 25th–75th percentiles.
The full program is ready. Each read takes 200 ms.
End-to-end time
Dots: medians. Bars: 25th–75th percentiles.
Research direction
What should stay in memory while a run waits?
Research direction
Research direction
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().
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))
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
With 28 GiB memory available
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.