sandbox: enable sandboxing for llama.cpp processes on macOS

Signed-off-by: Jacob Howard <jacob.howard@docker.com>
This commit is contained in:
Jacob Howard 2025-08-29 13:42:41 -06:00
parent 229e081bc2
commit 9741e9a734
No known key found for this signature in database
GPG Key ID: 3E8B8F7FEB46FC66
4 changed files with 160 additions and 5 deletions

View File

@ -9,7 +9,6 @@ import (
"io/fs"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
@ -24,6 +23,7 @@ import (
"github.com/docker/model-runner/pkg/inference/config"
"github.com/docker/model-runner/pkg/inference/models"
"github.com/docker/model-runner/pkg/logging"
"github.com/docker/model-runner/pkg/sandbox"
"github.com/docker/model-runner/pkg/tailbuffer"
)
@ -153,11 +153,15 @@ func (l *llamaCpp) Run(ctx context.Context, socket, model string, mode inference
}
l.log.Infof("llamaCppArgs: %v", args)
llamaCppProcess := exec.CommandContext(
llamaCppProcess, err := sandbox.CommandContext(
ctx,
sandbox.LlamaCppTemplate,
filepath.Join(binPath, "com.docker.llama-server"),
args...,
)
if err != nil {
return fmt.Errorf("unable to create sandboxed llama.cpp process: %w", err)
}
llamaCppProcess.Cancel = func() error {
if runtime.GOOS == "windows" {
return llamaCppProcess.Process.Kill()
@ -351,13 +355,19 @@ func (l *llamaCpp) checkGPUSupport(ctx context.Context) bool {
if l.updatedLlamaCpp {
binPath = l.updatedServerStoragePath
}
out, err := exec.CommandContext(
llamaCppProcess, err := sandbox.CommandContext(
ctx,
sandbox.LlamaCppTemplate,
filepath.Join(binPath, "com.docker.llama-server"),
"--list-devices",
).CombinedOutput()
)
if err != nil {
l.log.Warnf("Failed to determine if llama-server is built with GPU support: %s", err)
l.log.Warnf("Failed to create sandboxed llama.cpp process to probe GPU support: %v", err)
return false
}
out, err := llamaCppProcess.CombinedOutput()
if err != nil {
l.log.Warnf("Failed to determine if llama-server is built with GPU support: %v", err)
return false
}
sc := bufio.NewScanner(strings.NewReader(string(out)))

View File

@ -0,0 +1,111 @@
package sandbox
import (
"context"
"fmt"
"os"
"os/exec"
"os/user"
"strings"
)
// LlamaCppTemplate is the sandbox template to use for llama.cpp processes.
const LlamaCppTemplate = `(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.
(deny network*)
(allow network-bind network-inbound
(regex #"inference-runner-[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: 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")
(subpath "/Applications/Docker.app/Contents/Resources/model-runner")
(subpath "[HOMEDIR]/.docker/bin/inference")
(subpath "[HOMEDIR]/.docker/bin/lib"))
(allow file-write*
(regex #"inference-runner-[0-9]+\.sock$")
(literal "/dev/null")
(subpath "/private/var")
(subpath "[WORKDIR]"))
(allow file-read*
(subpath "[WORKDIR]")
(subpath "[HOMEDIR]/.docker/models"))
`
// CommandContext creates a sandboxed version of an os/exec.Cmd. On Darwin, we
// use the sandbox-exec command to wrap the process.
func CommandContext(ctx context.Context, template, name string, args ...string) (*exec.Cmd, 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)
}
// Compute the profile. Switch to text/template if this gets more complex.
profile := strings.ReplaceAll(template, "[HOMEDIR]", currentUser.HomeDir)
profile = strings.ReplaceAll(profile, "[WORKDIR]", currentDirectory)
// Create the sandboxed process.
sandboxedArgs := make([]string, 0, len(args)+3)
sandboxedArgs = append(sandboxedArgs, "-p", profile, name)
sandboxedArgs = append(sandboxedArgs, args...)
return exec.CommandContext(ctx, "sandbox-exec", sandboxedArgs...), nil
}

View File

@ -0,0 +1,18 @@
//go:build !darwin && !windows
package sandbox
import (
"context"
"os/exec"
)
// LlamaCppTemplate is the sandbox template to use for llama.cpp processes.
const LlamaCppTemplate = ``
// CommandContext creates a sandboxed version of an os/exec.Cmd. On Linux, we
// don't currently support sandboxing since we already run inside containers, so
// this function is a direct passthrough.
func CommandContext(ctx context.Context, template, name string, args ...string) (*exec.Cmd, error) {
return exec.CommandContext(ctx, name, args...), nil
}

View File

@ -0,0 +1,16 @@
package sandbox
import (
"context"
"os/exec"
)
// LlamaCppTemplate is the sandbox template to use for llama.cpp processes.
const LlamaCppTemplate = ``
// CommandContext creates a sandboxed version of an os/exec.Cmd. On Windows, we
// use the wsb exec command to wrap the process.
func CommandContext(ctx context.Context, template, name string, args ...string) (*exec.Cmd, error) {
// TODO: Implement.
return exec.CommandContext(ctx, name, args...), nil
}