model-runner/pkg/sandbox/sandbox_darwin.go

159 lines
5.1 KiB
Go

package sandbox
import (
"context"
"fmt"
"os"
"os/exec"
"os/user"
"strings"
)
// ConfigurationLlamaCpp is the sandbox configuration for llama.cpp processes.
const ConfigurationLlamaCpp = `(version 1)
;;; Keep a default allow policy (because encoding things like DYLD support and
;;; device access is quite difficult), but deny critical exploitation targets
;;; (generally aligned with the App Sandbox entitlements that aren't on by
;;; default). In theory we'll be subject to the Docker.app sandbox as well
;;; (unless we're running standalone), but even Docker.app has a very privileged
;;; sandbox, so we need additional constraints.
;;;
;;; Note: The following are known to be required at some level for llama.cpp
;;; (though we could further experiment to deny certain sub-permissions):
;;; - authorization
;;; - darwin
;;; - iokit
;;; - mach
;;; - socket
;;; - syscall
;;; - process
(allow default)
;;; Deny network access, except for our IPC sockets.
;;; NOTE: We use different socket nomenclature when running in Docker Desktop
;;; (inference-N.sock) vs. standalone (inference-runner-N.sock), so we use a
;;; wildcard to support both.
(deny network*)
(allow network-bind network-inbound
(regex #"inference.*-[0-9]+\.sock$"))
;;; Deny access to the camera and microphone.
(deny device*)
;;; Deny access to NVRAM settings.
(deny nvram*)
;;; Deny access to system-level privileges.
(deny system*)
;;; Deny access to job creation.
(deny job-creation)
;;; Don't allow new executable code to be created in memory at runtime.
(deny dynamic-code-generation)
;;; Disable access to user preferences.
(deny user-preference*)
;;; Restrict file access.
;;; NOTE: For some reason, the (home-subpath "...") predicate used in system
;;; sandbox profiles doesn't work with sandbox-exec.
;;; NOTE: We have to allow access to the working directory for standalone mode.
;;; NOTE: We have to allow access to a regex-based Docker.app location to
;;; support Docker Desktop development as well as Docker.app installs that don't
;;; live inside /Applications.
;;; NOTE: For some reason (deny file-read*) really doesn't like to play nice
;;; with llama.cpp, so for that reason we'll avoid a blanket ban and just ban
;;; directories that might contain sensitive data.
(deny file-map-executable)
(deny file-write*)
(deny file-read*
(subpath "/Applications")
(subpath "/private/etc")
(subpath "/Library")
(subpath "/Users")
(subpath "/Volumes"))
(allow file-read* file-map-executable
(subpath "/usr")
(subpath "/System")
(regex #"Docker\.app/Contents/Resources/model-runner")
(subpath "[HOMEDIR]/.docker/bin/inference")
(subpath "[HOMEDIR]/.docker/bin/lib"))
(allow file-write*
(literal "/dev/null")
(subpath "/private/var")
(subpath "[HOMEDIR]/Library/Containers/com.docker.docker/Data")
(subpath "[WORKDIR]"))
(allow file-read*
(subpath "[HOMEDIR]/.docker/models")
(subpath "[HOMEDIR]/Library/Containers/com.docker.docker/Data")
(subpath "[WORKDIR]"))
`
// sandbox is the Darwin sandbox implementation.
type sandbox struct {
// cancel cancels the context associated with the process.
cancel context.CancelFunc
// command is the sandboxed process handle.
command *exec.Cmd
}
// Command implements Sandbox.Command.
func (s *sandbox) Command() *exec.Cmd {
return s.command
}
// Command implements Sandbox.Close.
func (s *sandbox) Close() error {
s.cancel()
return nil
}
// New creates a new sandbox containing a single process that has been started.
// The ctx, name, and arg arguments correspond to their counterparts in
// os/exec.CommandContext. The configuration argument specifies the sandbox
// configuration, for which a pre-defined value should be used. The modifier
// function allows for an optional callback (which may be nil) to configure the
// command before it is started.
func New(ctx context.Context, configuration string, modifier func(*exec.Cmd), name string, arg ...string) (Sandbox, error) {
// Look up the user's home directory.
currentUser, err := user.Current()
if err != nil {
return nil, fmt.Errorf("unable to lookup user: %w", err)
}
// Look up the working directory.
currentDirectory, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("unable to determine working directory: %w", err)
}
// Process template arguments in the configuration. We should switch to
// text/template if this gets any more complex.
profile := strings.ReplaceAll(configuration, "[HOMEDIR]", currentUser.HomeDir)
profile = strings.ReplaceAll(profile, "[WORKDIR]", currentDirectory)
// Create a subcontext we can use to regulate the process lifetime.
ctx, cancel := context.WithCancel(ctx)
// Create and configure the command.
sandboxedArgs := make([]string, 0, len(arg)+3)
sandboxedArgs = append(sandboxedArgs, "-p", profile, name)
sandboxedArgs = append(sandboxedArgs, arg...)
command := exec.CommandContext(ctx, "sandbox-exec", sandboxedArgs...)
if modifier != nil {
modifier(command)
}
// Start the process.
if err := command.Start(); err != nil {
cancel()
return nil, fmt.Errorf("unabled to start sandboxed process: %w", err)
}
return &sandbox{
cancel: cancel,
command: command,
}, nil
}