Created
June 4, 2023 18:02
-
-
Save lucaswiman/1cec6584015149f0df1bb24c875a0709 to your computer and use it in GitHub Desktop.
MacOS Sandbox for Python Code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash -e | |
# set -x | |
# References: | |
# * https://wiki.mozilla.org/Sandbox/OS_X_Rule_Set | |
# * https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf | |
# * https://mybyways.com/blog/creating-a-macos-sandbox-to-run-kodi (sound) | |
# * See also existing rulesets in `/usr/share/sandbox` | |
# * https://github.com/devpi/devpi (pypi proxy) | |
echo args="$@" | |
function unused-port { | |
local PORT | |
while : | |
do | |
PORT="`shuf -i 2000-65000 -n 1`" | |
lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null || { echo $PORT; return 0; } | |
done | |
} | |
directories_to_delete=() | |
# Note: there were some permissions issues with `mktemp -d` where | |
# the sandboxed process couldn't access it. Put the temp dir in PWD, | |
# where we are already giving read/write access that works. | |
app_temp_dir=$PWD/.tmp | |
mkdir -p $app_temp_dir | |
export TMPDIR="$app_temp_dir" | |
export DARWIN_USER_TEMP_DIR="$TMPDIR" | |
directories_to_delete+=("$app_temp_dir") | |
directories_to_read=() | |
NETWORK_RULES="" | |
if [ "$1" == "--python" ]; then | |
shift | |
if [ -z "$(which "devpi-server")" ]; then | |
echo 'devpi must be installed to use devpi-server: `pipx install devpi`' >&2 | |
exit 1 | |
fi | |
PYPI_PROXY_PORT=$(unused-port) | |
devpi_temp_dir=$(mktemp -d) | |
directories_to_delete+=("$devpi_temp_dir") | |
devpi-init --serverdir=$devpi_temp_dir | |
echo "Serving pypi proxy on http://127.0.0.1:$PYPI_PROXY_PORT" | |
devpi-server --serverdir=$devpi_temp_dir --port=$PYPI_PROXY_PORT & | |
export PIP_INDEX_URL="http://127.0.0.1:$PYPI_PROXY_PORT/root/pypi/+simple/" | |
NETWORK_RULES+=' | |
(allow network-outbound (remote ip "localhost:'$PYPI_PROXY_PORT'")) | |
' | |
echo "Waiting for port $port to be available..." | |
while ! nc -z localhost $PYPI_PROXY_PORT; do | |
sleep 1 | |
done | |
echo | |
if [ ! -d ".venv" ]; then | |
echo "Creating .venv in the current directory" | |
python3 -m venv --copies .venv | |
fi | |
directories_to_read+=("$(python -c "import sys; print(sys.base_prefix)")") | |
fi | |
echo args="$@" | |
audio_rules="" | |
if [ "$1" == "--audio" ]; then | |
## Usage: local-sandbox --audio say hello | |
shift | |
audio_rules=' | |
; Audio. See https://mybyways.com/blog/creating-a-macos-sandbox-to-run-kodi | |
(allow mach* sysctl-read) | |
(allow ipc-posix-shm | |
(ipc-posix-name-regex "^AudioIO")) | |
' | |
fi | |
if [ "$1" == "--network-outbound" ]; then | |
shift | |
NETWORK_RULES+=" (allow network-outbound)" | |
fi | |
# https://stackoverflow.com/a/22644006/303931 | |
trap "exit" INT TERM | |
trap cleanup EXIT | |
function kill_recursive() { | |
local pid=$1 | |
local child_pids=$(pgrep -P $pid) | |
for child_pid in $child_pids; do | |
kill_recursive $child_pid | |
done | |
kill $pid | |
} | |
function cleanup { | |
for dir in "${directories_to_delete[@]}"; do | |
# Check if directory exists | |
if [ -d "$dir" ]; then | |
echo "Deleting directory: $dir" | |
rm -r "$dir" | |
else | |
echo "Directory does not exist: $dir" | |
fi | |
done | |
kill_recursive 0 | |
} | |
function construct-file-hierarchy-rules { | |
# Weird things happen when you don't have permissions to ancestor directories | |
# of PWD, e.g. `bash` will say: | |
# > shell-init: error retrieving current directory: getcwd: cannot access parent directories: Operation not permitted | |
# Here we give read access to _only_ the directory, not files contained in it. | |
local dir="$1" | |
local dirs_to_process=("$1") | |
if [[ "$1" == /tmp/* ]]; then | |
echo "$1 starts with /tmp/; also granting access to /private/$1" >&2 | |
dirs_to_process+=("/private$1") | |
fi | |
local dirs=() | |
for dir in "${dirs_to_process[@]}"; do | |
if [ -d "$dir" ]; then | |
dirs+=("$dir") | |
fi | |
while [[ "$dir" != "" && "$dir" != "/" ]]; do | |
dirs+=("$(dirname "$dir")") | |
dir="$(dirname "$dir")" | |
done | |
done | |
local policy='(allow file-read* ' | |
for dir in "${dirs[@]}"; do | |
policy+="(literal \"$dir\") " | |
done | |
policy+=")" | |
for dir in "${dirs_to_process[@]}"; do | |
policy+=" (allow file* (subpath \"$dir\"))" | |
done | |
echo "$policy" | |
} | |
directories_rules="" | |
for directory_to_read in "${directories_to_read[@]}"; do | |
directories_rules+="(subpath \"$directory_to_read\") " | |
done | |
if [[ "$directories_rules" != "" ]]; then | |
directories_rules="(allow file-read* $directories_rules)" | |
fi | |
PROFILE='(version 1) | |
(deny default) | |
(deny file*) | |
(allow process*) | |
; Allow arrow keys to work correctly. | |
(allow pseudo-tty) | |
(allow file-ioctl | |
(literal "/dev/ptmx") | |
(regex #"^/dev/ttys") | |
) | |
; needed for os.uname() | |
(allow sysctl-read) | |
(allow file-read* | |
(literal "/") ; Needed to make many file operations work, even ls-ing a directory you have file* permissions in. | |
) | |
;;;; Based on mozilla ruleset above. ;;;; | |
(allow file-read-metadata | |
(literal "/etc") | |
(literal "/var") | |
(literal "/private/etc/localtime") | |
) | |
(allow file-read* | |
(literal "/dev/autofs_nowait") | |
(literal "/dev/random") | |
(literal "/dev/urandom") | |
) | |
(allow file-read* | |
file-write-data | |
(literal "/dev/null") | |
(literal "/dev/zero") | |
) | |
; Allow reading from globally readable files. | |
(allow file-read* | |
(require-all | |
(require-any | |
(file-mode #o0004) ; From the mozilla example | |
(file-mode #o0755) ; For some reason needed to read clang/clang++ | |
) | |
(require-any | |
(subpath "/Library/Filesystems/NetFSPlugins") | |
(subpath "/System") | |
(subpath "/usr/lib") | |
(subpath "/usr/bin") | |
(subpath "/bin") | |
(subpath "/usr/share") | |
;;; Xcode command line tool requirements: | |
(subpath "/Applications/Xcode.app") | |
(subpath "/Library/Developer") | |
(subpath "/private/var/select") | |
) | |
) | |
) | |
;;;; End of mozilla-inspired rules. ;;;; | |
; Needed for clang to work, presumably the ls not working with parent directories inaccessible thing. | |
(allow file-read* | |
(literal "/Library") | |
) | |
(allow file* | |
(subpath "'$app_temp_dir'") | |
) | |
'"$NETWORK_RULES"' | |
'"$(construct-file-hierarchy-rules "$PWD")"' | |
'"$directories_rules"' | |
'"$audio_rules" | |
echo "$PROFILE" | |
sandbox-exec -p "$PROFILE" "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment