Skip to content

Commit a4b108e

Browse files
committed
Remove asteval from graalos demo story, it's confusing
1 parent 3d6baa2 commit a4b108e

3 files changed

Lines changed: 39 additions & 99 deletions

File tree

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,24 @@
11
# GraalOS Standalone Sandbox Demo
22

3-
This demo shows a small chat-style expression evaluator running inside the
3+
This demo shows a small chat-style Python evaluator running inside the
44
GraalPy GraalOS standalone.
55

66
The story:
77

88
1. `rich` renders a friendly terminal UI.
9-
2. `asteval` evaluates normal user expressions with an application-level
10-
restricted evaluator. This demo also removes `open` from that evaluator.
11-
3. `/unsafe ...` expressions deliberately bypass `asteval` and use Python
12-
`eval` directly.
13-
4. The process is still inside the GraalOS sandbox, so file, subprocess,
9+
2. The demo treats each entered expression as untrusted Python code, such as
10+
code produced by an LLM agent or pasted by a human operator.
11+
3. The process is inside the GraalOS sandbox, so file, subprocess,
1412
network, and native library attempts remain contained.
1513

1614
## Setup
1715

18-
From the standalone directory, install `pip` inside the sandbox, then
19-
install the demo wheels from a host-downloaded wheel cache:
20-
21-
```bash
22-
./bin/graalpy -Im ensurepip
23-
python3 -m pip download --only-binary=:all: --dest demo-wheels rich asteval
24-
./bin/graalpy -Im pip install --no-index --find-links /demo-wheels rich asteval
25-
```
26-
27-
The online download is intentionally done outside the sandbox. The sandboxed
28-
standalone has no outbound network mapping by default, which is one of the
29-
things the demo can show.
30-
31-
### GRAALOS-8260 workaround
32-
33-
There is currently a bug in GraalOS that prevents the above ensurepip command
34-
from working. Until it is fixed, we can install the pure-Python wheels from the
35-
host directly into the standalone's `site-packages`:
16+
We can install the `rich` wheel directly into the standalone's `site-packages`
17+
using any standard Python. While we could run `ensurepip` and `pip` inside the
18+
sandbox by configuring the appropriate network access, we do this here
19+
intentionally done outside the sandbox. The sandboxed standalone has no
20+
outbound network mapping by default, which is one of the things the demo can
21+
show.
3622

3723
```bash
3824
python3 -m pip install \
@@ -43,12 +29,9 @@ python3 -m pip install \
4329
--abi none --abi graalpy250_312_native \
4430
--platform any --platform graalos_x86_64 \
4531
--no-compile \
46-
rich asteval
32+
rich
4733
```
4834

49-
Use this workaround only for pure-Python wheels such as `py3-none-any`; native
50-
wheels need GraalOS/GraalPy-specific handling.
51-
5235
There should be a file `test_graalos_sandbox_chat.py` in this directory. If
5336
not, find it in and copy it from the GraalPy source repository. From inside the
5437
sandbox that file is available as `/test_graalos_sandbox_chat.py`, so run:
@@ -71,21 +54,15 @@ Start with a normal expression:
7154
sum([i*i for i in range(1000)])
7255
```
7356

74-
Then show that the app-level evaluator blocks direct file access:
57+
Then move on to untrusted code that tries to access host resources:
7558

7659
```python
7760
open('/etc/passwd').read()
78-
```
79-
80-
Switch to `/unsafe` mode to bypass the app-level evaluator while keeping the
81-
outer GraalOS sandbox:
82-
83-
```python
84-
/unsafe open('/etc/passwd').read().splitlines()[:3]
85-
/unsafe open('/etc/shadow').read()
86-
/unsafe __import__('subprocess').run(['/bin/sh', '-c', 'id'], capture_output=True, text=True)
87-
/unsafe __import__('socket').create_connection(('example.com', 80), timeout=2)
88-
/unsafe __import__('ctypes').CDLL('libc.so').system(b'cat /etc/shadow')
61+
open('/etc/passwd').read().splitlines()[:3]
62+
open('/etc/shadow').read()
63+
__import__('subprocess').run(['/bin/sh', '-c', 'id'], capture_output=True, text=True)
64+
__import__('socket').create_connection(('example.com', 80), timeout=2)
65+
__import__('ctypes').CDLL('libc.so').system(b'cat /etc/shadow')
8966
```
9067

9168
Expected result: harmless operations work or fail normally; sensitive host
@@ -95,7 +72,7 @@ filesystem, process namespace, and configured network policy. The native
9572

9673
## Why This Is Useful
9774

98-
`asteval` is an application-level guardrail. It reduces accidental exposure but
99-
it is not a complete containment boundary. GraalOS is the outer boundary: even
100-
if application logic accidentally evaluates dangerous code in `/unsafe` mode,
101-
the runtime still mediates filesystem, subprocess, native, and network behavior.
75+
This is a deliberately unsafe application pattern: it evaluates untrusted Python
76+
code directly. That is useful for demonstrating the actual containment boundary.
77+
GraalOS is that boundary, and it mediates filesystem, subprocess, native, and
78+
network behavior even when the application itself offers no extra guardrails.

graalpython/com.oracle.graal.python.test/src/tests/graalos/test_graalos_sandbox_chat.py

Lines changed: 18 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3838
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3939
# SOFTWARE.
40-
"""Small Rich + asteval demo for the GraalOS standalone sandbox."""
40+
"""Small Rich demo for the GraalOS standalone sandbox."""
4141

4242
from __future__ import annotations
4343

@@ -59,39 +59,12 @@ class EvalResult:
5959
ok: bool
6060
output: str
6161
elapsed_ms: float
62-
63-
64-
def make_interpreter():
65-
from asteval import Interpreter
66-
aeval = Interpreter()
67-
# Keep the demo's "safe" lane expression-oriented. GraalOS is the real
68-
# containment boundary; this prevents the app-level evaluator from opening files.
69-
aeval.symtable.pop("open", None)
70-
return aeval
71-
72-
7362
def render_message(role: str, body: str, style: str) -> None:
7463
from rich.panel import Panel
7564
from rich.text import Text
7665
console.print(Panel(Text(body), title=role, title_align="left", border_style=style))
7766

7867

79-
def safe_eval(aeval, expr: str):
80-
start = time.perf_counter()
81-
aeval.error = []
82-
try:
83-
value = aeval(expr)
84-
except Exception as exc: # asteval normally records errors instead of raising
85-
elapsed = (time.perf_counter() - start) * 1000
86-
return EvalResult("asteval", False, f"{type(exc).__name__}: {exc}", elapsed)
87-
88-
elapsed = (time.perf_counter() - start) * 1000
89-
if aeval.error:
90-
errors = "\n".join(str(err.get_error()) for err in aeval.error)
91-
return EvalResult("asteval", False, errors, elapsed)
92-
return EvalResult("asteval", True, repr(value), elapsed)
93-
94-
9568
def unsafe_eval(expr: str):
9669
start = time.perf_counter()
9770
try:
@@ -117,27 +90,23 @@ def render_result(result) -> None:
11790
render_message("sandbox", result.output, "green" if result.ok else "red")
11891

11992

120-
def evaluate(aeval, line: str) -> None:
93+
def evaluate(line: str) -> None:
12194
line = line.strip()
12295
if not line:
12396
return
124-
if line.startswith("/unsafe "):
125-
expr = line[len("/unsafe ") :].strip()
126-
render_result(unsafe_eval(expr))
127-
else:
128-
render_result(safe_eval(aeval, line))
97+
render_result(unsafe_eval(line))
12998

13099

131100
def demo_script() -> list[str]:
132101
return [
133102
"sum([i*i for i in range(1000)])",
134103
"sin(pi / 4) ** 2 + cos(pi / 4) ** 2",
135104
"open('/etc/passwd').read()",
136-
"/unsafe open('/etc/passwd').read().splitlines()[:3]",
137-
"/unsafe open('/etc/shadow').read()",
138-
"/unsafe __import__('subprocess').run(['/bin/sh', '-c', 'id'], capture_output=True, text=True)",
139-
"/unsafe __import__('socket').create_connection(('example.com', 80), timeout=2)",
140-
"/unsafe __import__('ctypes').CDLL('libc.so').system(b'cat /etc/shadow')",
105+
"open('/etc/passwd').read().splitlines()[:3]",
106+
"open('/etc/shadow').read()",
107+
"__import__('subprocess').run(['/bin/sh', '-c', 'id'], capture_output=True, text=True)",
108+
"__import__('socket').create_connection(('example.com', 80), timeout=2)",
109+
"__import__('ctypes').CDLL('libc.so').system(b'cat /etc/shadow')",
141110
]
142111

143112

@@ -146,10 +115,10 @@ def print_intro() -> None:
146115
"""
147116
Type Python expressions and get chat-style results.
148117
149-
Normal input uses asteval, a restricted expression evaluator.
150-
Prefix with /unsafe to bypass asteval and use Python eval directly.
151-
The process is still inside the GraalOS sandbox, so filesystem,
152-
subprocess, native library, and network attempts remain contained.
118+
This demo treats each expression as untrusted Python code, such as
119+
code proposed by an LLM agent or pasted by a human operator.
120+
GraalOS sandboxes that code, so filesystem, subprocess, native
121+
library, and network attempts remain contained.
153122
154123
Commands: /demo, /help, /quit
155124
"""
@@ -164,7 +133,6 @@ def print_help() -> None:
164133

165134

166135
def interactive() -> int:
167-
aeval = make_interpreter()
168136
print_intro()
169137
while True:
170138
try:
@@ -179,22 +147,20 @@ def interactive() -> int:
179147
print_help()
180148
continue
181149
if command == "/demo":
182-
run_demo(aeval)
150+
run_demo()
183151
continue
184-
evaluate(aeval, line)
152+
evaluate(line)
185153

186154

187-
def run_demo(aeval=None) -> None:
188-
if aeval is None:
189-
aeval = make_interpreter()
155+
def run_demo() -> None:
190156
for line in demo_script():
191157
render_message("you", line, "blue")
192-
evaluate(aeval, line)
158+
evaluate(line)
193159

194160

195161
def main(argv: list[str] | None = None) -> int:
196-
global console, Console
197162
from rich.console import Console
163+
global console
198164
console = Console()
199165
parser = argparse.ArgumentParser()
200166
parser.add_argument("--demo", action="store_true", help="run the prepared demo script and exit")
@@ -219,13 +185,12 @@ def setUp(self):
219185
skip_unless_graalos()
220186

221187
def test_demo_packages(self):
222-
import asteval
223188
import rich
224189

225-
self.assertTrue(asteval.__version__)
226190
self.assertTrue(rich.get_console())
227191

228192
def test_sandbox_chat_demo(self):
193+
from rich.console import Console
229194
global console
230195
output = io.StringIO()
231196
console = Console(file=output, force_terminal=False, color_system=None, width=120)

mx.graalpython/mx_graalpython_graalos.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ def _prepare_graalos_demo(standalone_home: Path, env):
207207
"--platform", "any",
208208
"--dest", str(demo_wheels),
209209
"rich",
210-
"asteval",
211210
], env=env)
212211
# Work around GRAALOS-8260 by installing pure-Python demo wheels from the
213212
# host. Remove this once in-sandbox ensurepip/pip subprocesses work there.
@@ -220,7 +219,6 @@ def _prepare_graalos_demo(standalone_home: Path, env):
220219
"--no-compile",
221220
"--upgrade",
222221
"rich",
223-
"asteval",
224222
], env=env)
225223

226224
from mx_graalpython import _python_unittest_root

0 commit comments

Comments
 (0)