LLMのPythonスクリプト出力を比較的安全に実行したい、そういう時って割とありますよね。使うのは自分だけなので怖いのは偶然危険なコードが出てきた時だけ、みたいなケースです。
でも世間に出回ってる情報はDockerを使ったもの (llm-sandbox) とか、WebAssemblyを使ったもの (Pyodide) とか、CPython以外の実装を使ったもの (PyPyのサンドボックス機能) とか、Webサービス向けの重量級のものばかり。軽かったであろう pysandbox はデザインに欠陥があり、制限も多く開発中止となっていました。
そんな中でさてどうしよう、サンドボックスのPython環境でもホストのPython環境と同じものが使えれば管理が楽なのに、と思いながら調べていました。
その結果、Pythonライブラリの中では決定打が見つからなかったものの、Linux環境の中では systemd-nspawn を読み込み専用マウントで使う方法を思いつき(但しルート権限が必要)、その systemd-nspawn に似ながらもルート権限が不要なbubblewrap (bwrap) を見つけ、その bwrap を使ってサンドボックスで Python スクリプトを実行するコードを書いてみました。
import os
import subprocess
home_dir = os.path.expanduser("~")
try:
os.makedirs("/tmp/mysandbox_root")
except FileExistsError:
pass
def sandbox_pyexec(code):
proc = subprocess.Popen(
["bwrap", "--bind", "/tmp/mysandbox_root", "/", "--ro-bind", "/bin", "/bin", # システムファイルをRead-Onlyで共有する
"--ro-bind", "/lib", "/lib", "--ro-bind", "/lib64", "/lib64",
"--ro-bind", "/usr", "/usr", "--ro-bind", "/etc", "/etc", # あんまり良くない
"--ro-bind", home_dir + "/.local/lib", home_dir + "/.local/lib", # pip ライブラリをRead-Onlyで共有する
"--ro-bind", home_dir + "/.local/bin", home_dir + "/.local/bin",
"--ro-bind", home_dir + "/.local/share", home_dir + "/.local/share",
"--bind", "/proc", "/proc", "--unshare-all", "--", # なるべくホストと非共有(systemd-nspawnと違ってプロセスリストは共有してるけど)
"bash", "-c", "python -"], # Pythonで標準入力のコードを実行
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True # stdin / stdout を文字列として扱う
)
result, err = proc.communicate(code)
return (result.strip(), err.strip())
r, err = sandbox_pyexec( """print("これはテストです")""")
print("stdout:", r)
print("stderr:", err)
# stdout: これはテストです
# stderr:
r, err = sandbox_pyexec( """import os
os.system("ls /")""")
print("stdout:", r)
print("stderr:", err)
# stdout: bin
# etc
# home
# lib
# lib64
# proc
# usr
# stderr:
r, err = sandbox_pyexec( """wrong python syntax here""")
print("stdout:", r)
print("stderr:", err)
# stdout:
# stderr: File "<stdin>", line 1
# wrong python syntax here
# ^^^^^^
# SyntaxError: invalid syntax