|
| 1 | +package sandbox |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "os/exec" |
| 8 | + "os/user" |
| 9 | + "strings" |
| 10 | +) |
| 11 | + |
| 12 | +// ConfigurationLlamaCpp is the sandbox configuration for llama.cpp processes. |
| 13 | +const ConfigurationLlamaCpp = `(version 1) |
| 14 | +
|
| 15 | +;;; Keep a default allow policy (because encoding things like DYLD support and |
| 16 | +;;; device access is quite difficult), but deny critical exploitation targets |
| 17 | +;;; (generally aligned with the App Sandbox entitlements that aren't on by |
| 18 | +;;; default). In theory we'll be subject to the Docker.app sandbox as well |
| 19 | +;;; (unless we're running standalone), but even Docker.app has a very privileged |
| 20 | +;;; sandbox, so we need additional constraints. |
| 21 | +;;; |
| 22 | +;;; Note: The following are known to be required at some level for llama.cpp |
| 23 | +;;; (though we could further experiment to deny certain sub-permissions): |
| 24 | +;;; - authorization |
| 25 | +;;; - darwin |
| 26 | +;;; - iokit |
| 27 | +;;; - mach |
| 28 | +;;; - socket |
| 29 | +;;; - syscall |
| 30 | +;;; - process |
| 31 | +(allow default) |
| 32 | +
|
| 33 | +;;; Deny network access, except for our IPC sockets. |
| 34 | +;;; NOTE: We use different socket nomenclature when running in Docker Desktop |
| 35 | +;;; (inference-N.sock) vs. standalone (inference-runner-N.sock), so we use a |
| 36 | +;;; wildcard to support both. |
| 37 | +(deny network*) |
| 38 | +(allow network-bind network-inbound |
| 39 | + (regex #"inference.*-[0-9]+\.sock$")) |
| 40 | +
|
| 41 | +;;; Deny access to the camera and microphone. |
| 42 | +(deny device*) |
| 43 | +
|
| 44 | +;;; Deny access to NVRAM settings. |
| 45 | +(deny nvram*) |
| 46 | +
|
| 47 | +;;; Deny access to system-level privileges. |
| 48 | +(deny system*) |
| 49 | +
|
| 50 | +;;; Deny access to job creation. |
| 51 | +(deny job-creation) |
| 52 | +
|
| 53 | +;;; Don't allow new executable code to be created in memory at runtime. |
| 54 | +(deny dynamic-code-generation) |
| 55 | +
|
| 56 | +;;; Disable access to user preferences. |
| 57 | +(deny user-preference*) |
| 58 | +
|
| 59 | +;;; Restrict file access. |
| 60 | +;;; NOTE: For some reason, the (home-subpath "...") predicate used in system |
| 61 | +;;; sandbox profiles doesn't work with sandbox-exec. |
| 62 | +;;; NOTE: We have to allow access to the working directory for standalone mode. |
| 63 | +;;; NOTE: We have to allow access to a regex-based Docker.app location to |
| 64 | +;;; support Docker Desktop development as well as Docker.app installs that don't |
| 65 | +;;; live inside /Applications. |
| 66 | +;;; NOTE: For some reason (deny file-read*) really doesn't like to play nice |
| 67 | +;;; with llama.cpp, so for that reason we'll avoid a blanket ban and just ban |
| 68 | +;;; directories that might contain sensitive data. |
| 69 | +(deny file-map-executable) |
| 70 | +(deny file-write*) |
| 71 | +(deny file-read* |
| 72 | + (subpath "/Applications") |
| 73 | + (subpath "/private/etc") |
| 74 | + (subpath "/Library") |
| 75 | + (subpath "/Users") |
| 76 | + (subpath "/Volumes")) |
| 77 | +(allow file-read* file-map-executable |
| 78 | + (subpath "/usr") |
| 79 | + (subpath "/System") |
| 80 | + (regex #"Docker\.app/Contents/Resources/model-runner") |
| 81 | + (subpath "[HOMEDIR]/.docker/bin/inference") |
| 82 | + (subpath "[HOMEDIR]/.docker/bin/lib")) |
| 83 | +(allow file-write* |
| 84 | + (literal "/dev/null") |
| 85 | + (subpath "/private/var") |
| 86 | + (subpath "[HOMEDIR]/Library/Containers/com.docker.docker/Data") |
| 87 | + (subpath "[WORKDIR]")) |
| 88 | +(allow file-read* |
| 89 | + (subpath "[HOMEDIR]/.docker/models") |
| 90 | + (subpath "[HOMEDIR]/Library/Containers/com.docker.docker/Data") |
| 91 | + (subpath "[WORKDIR]")) |
| 92 | +` |
| 93 | + |
| 94 | +// sandbox is the Darwin sandbox implementation. |
| 95 | +type sandbox struct { |
| 96 | + // cancel cancels the context associated with the process. |
| 97 | + cancel context.CancelFunc |
| 98 | + // command is the sandboxed process handle. |
| 99 | + command *exec.Cmd |
| 100 | +} |
| 101 | + |
| 102 | +// Command implements Sandbox.Command. |
| 103 | +func (s *sandbox) Command() *exec.Cmd { |
| 104 | + return s.command |
| 105 | +} |
| 106 | + |
| 107 | +// Command implements Sandbox.Close. |
| 108 | +func (s *sandbox) Close() error { |
| 109 | + s.cancel() |
| 110 | + return nil |
| 111 | +} |
| 112 | + |
| 113 | +// Create creates a sandbox containing a single process that has been started. |
| 114 | +// The ctx, name, and arg arguments correspond to their counterparts in |
| 115 | +// os/exec.CommandContext. The configuration argument specifies the sandbox |
| 116 | +// configuration, for which a pre-defined value should be used. The modifier |
| 117 | +// function allows for an optional callback (which may be nil) to configure the |
| 118 | +// command before it is started. |
| 119 | +func Create(ctx context.Context, configuration string, modifier func(*exec.Cmd), name string, arg ...string) (Sandbox, error) { |
| 120 | + // Look up the user's home directory. |
| 121 | + currentUser, err := user.Current() |
| 122 | + if err != nil { |
| 123 | + return nil, fmt.Errorf("unable to lookup user: %w", err) |
| 124 | + } |
| 125 | + |
| 126 | + // Look up the working directory. |
| 127 | + currentDirectory, err := os.Getwd() |
| 128 | + if err != nil { |
| 129 | + return nil, fmt.Errorf("unable to determine working directory: %w", err) |
| 130 | + } |
| 131 | + |
| 132 | + // Process template arguments in the configuration. We should switch to |
| 133 | + // text/template if this gets any more complex. |
| 134 | + profile := strings.ReplaceAll(configuration, "[HOMEDIR]", currentUser.HomeDir) |
| 135 | + profile = strings.ReplaceAll(profile, "[WORKDIR]", currentDirectory) |
| 136 | + |
| 137 | + // Create a subcontext we can use to regulate the process lifetime. |
| 138 | + ctx, cancel := context.WithCancel(ctx) |
| 139 | + |
| 140 | + // Create and configure the command. |
| 141 | + sandboxedArgs := make([]string, 0, len(arg)+3) |
| 142 | + sandboxedArgs = append(sandboxedArgs, "-p", profile, name) |
| 143 | + sandboxedArgs = append(sandboxedArgs, arg...) |
| 144 | + command := exec.CommandContext(ctx, "sandbox-exec", sandboxedArgs...) |
| 145 | + if modifier != nil { |
| 146 | + modifier(command) |
| 147 | + } |
| 148 | + |
| 149 | + // Start the process. |
| 150 | + if err := command.Start(); err != nil { |
| 151 | + cancel() |
| 152 | + return nil, fmt.Errorf("unable to start sandboxed process: %w", err) |
| 153 | + } |
| 154 | + return &sandbox{ |
| 155 | + cancel: cancel, |
| 156 | + command: command, |
| 157 | + }, nil |
| 158 | +} |
0 commit comments