Skip to content

Instantly share code, notes, and snippets.

@lucaswiman
Created June 4, 2023 18:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lucaswiman/1cec6584015149f0df1bb24c875a0709 to your computer and use it in GitHub Desktop.
Save lucaswiman/1cec6584015149f0df1bb24c875a0709 to your computer and use it in GitHub Desktop.
MacOS Sandbox for Python Code
#!/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