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?
acme = market.recent_price("ACME")beta = market.recent_price("BETA")portfolio_value = acme + beta
External call
External call
Local calculation
External operations are exposed as Python functions.
Load CPython moduleInit PythonInit PackagesSnapshot+ Module data
+ Python objects
+ NumPy objects
Saved linear memory
Load CPython moduleInit PythonInit PackagesSnapshot+ Module data
+ Python objects
+ NumPy objects
Saved linear memory
Load CPython moduleInit PythonInit PackagesSnapshot+ Module data
+ Python objects
+ NumPy objects
Saved linear memory
Load CPython moduleInit PythonInit PackagesSnapshot+ Module data
+ Python objects
+ NumPy objects
Saved linear memory
What happens when Mallory writes?
With 28 GiB memory available
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
These price reads leave market data unchanged. Prepare starts a request and returns a handle. It does not wait for the result.
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
Materialize waits here if the result is not ready. Python then uses the result and continues.
q_acme = prepare(market.recent_price, "ACME")q_beta = prepare(market.recent_price, "BETA")# ... independent computation ...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")# ... independent computation ...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")# ... independent computation ...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")# ... independent computation ...acme = materialize(linearize(q_acme))beta = materialize(linearize(q_beta))portfolio_value = acme + beta
Linearize checks the saved result at the original call site. The ticker must match and the quote must be at most five minutes old. Otherwise, request a new quote.
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
The Host tracks these requests and checks them at the original calls.
The full program is ready. Each synthetic 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 synthetic read takes 200 ms.
End-to-end time
The full program is ready. Each synthetic read takes 200 ms.
End-to-end time
acme = market.recent_price("ACME")......
The first complete line gives us the call and its input. The Host can prepare the ACME read while more code arrives.
acme = market.recent_price("ACME")beta = market.recent_price("BETA")portfolio_value = acme + beta
With the full source, we validate and run the program. At the ACME call, we check and reuse the early request.
Preparing requests during code generation reduced end-to-end time by about 22%, compared with waiting for the full source and then running serially.
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)
Pending
Wait at the call
Ready
Check and return
Cannot reuse
Call the tool now
ticker = "ACME"unknown_call()label = "Latest quote"price = market.recent_price(ticker)result = [label, price]
ticker = "ACME"unknown_call()q = prepare(market.recent_price, ticker)label = "Latest quote"price = materialize(linearize(q))result = [label, price]
unknown_call() has unknown effects. Prepare stays after it.
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()
An error at the original call stops the following write, preserving the program’s behaviour.
At the callErrorStop
a Arguments
t Call time
h Hidden state
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.