Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions pkg/distribution/internal/store/blobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package store

import (
"context"
"crypto/sha256"
"encoding/hex"
Comment on lines +5 to +6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The store allows both sha256 and sha512 algorithms (see line 25), but the verification logic only handles sha256. To prevent a security bypass where a malicious registry could serve unverified sha512 blobs, we should support verification for all allowed algorithms. This requires importing crypto/sha512 and hash.

Suggested change
"crypto/sha256"
"encoding/hex"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"

"errors"
"fmt"
"io"
Expand Down Expand Up @@ -268,6 +270,35 @@ func (s *LocalStore) WriteBlobWithResume(diffID oci.Hash, r io.Reader, digestStr

f.Close() // Rename will fail on Windows if the file is still open.

// Verify the digest of the completed file before making it visible. This
// must be done after closing the file (to flush all writes) and before the
// rename so that a mismatch never results in a corrupt blob being stored.
// We hash the whole file rather than the streamed bytes so that resumed
// downloads (which append to an existing partial file) are verified
// correctly over their entire contents.
if diffID.Algorithm == "sha256" {
completedFile, openErr := os.Open(incompletePath)
if openErr != nil {
_ = os.Remove(incompletePath)
return fmt.Errorf("open completed blob file for verification: %w", openErr)
}

hasher := sha256.New()
if _, copyErr := io.Copy(hasher, completedFile); copyErr != nil {
completedFile.Close()
_ = os.Remove(incompletePath)
return fmt.Errorf("hash completed blob file: %w", copyErr)
}
completedFile.Close()

computed := hex.EncodeToString(hasher.Sum(nil))
if computed != diffID.Hex {
_ = os.Remove(incompletePath)
return fmt.Errorf("blob digest mismatch for %q: expected sha256:%s, got sha256:%s",
diffID.String(), diffID.Hex, computed)
}
}
Comment on lines +279 to +300
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The verification logic is hardcoded to sha256, which means sha512 blobs (which are allowed by the store) will skip verification entirely. This creates a security vulnerability. The logic should be updated to use the appropriate hasher based on the diffID.Algorithm.

	if diffID.Algorithm == "sha256" || diffID.Algorithm == "sha512" {
		completedFile, openErr := os.Open(incompletePath)
		if openErr != nil {
			_ = os.Remove(incompletePath)
			return fmt.Errorf("open completed blob file for verification: %w", openErr)
		}

		var hasher hash.Hash
		if diffID.Algorithm == "sha512" {
			hasher = sha512.New()
		} else {
			hasher = sha256.New()
		}

		if _, copyErr := io.Copy(hasher, completedFile); copyErr != nil {
			completedFile.Close()
			_ = os.Remove(incompletePath)
			return fmt.Errorf("hash completed blob file: %w", copyErr)
		}
		completedFile.Close()

		computed := hex.EncodeToString(hasher.Sum(nil))
		if computed != diffID.Hex {
			_ = os.Remove(incompletePath)
			return fmt.Errorf("blob digest mismatch for %q: expected %s:%s, got %s:%s",
				diffID.String(), diffID.Algorithm, diffID.Hex, diffID.Algorithm, computed)
		}
	}


if renameFinalErr := os.Rename(incompletePath, path); renameFinalErr != nil {
return fmt.Errorf("rename blob file: %w", renameFinalErr)
}
Expand Down
32 changes: 30 additions & 2 deletions pkg/distribution/internal/store/blobs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,40 @@ func TestBlobs(t *testing.T) {
}
})

t.Run("WriteBlob reuses existing blob", func(t *testing.T) {
// simulate existing blob
t.Run("WriteBlob rejects blob with wrong digest", func(t *testing.T) {
// Use a well-known sha256 hash (of empty string) but supply different content.
hash := oci.Hash{
Algorithm: "sha256",
Hex: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
}
blobPath, err := store.blobPath(hash)
if err != nil {
t.Fatalf("error getting blob path: %v", err)
}
// Remove any pre-existing blob so we hit the download path.
_ = os.Remove(blobPath)
_ = os.Remove(incompletePath(blobPath))

if err := store.WriteBlob(hash, bytes.NewBufferString("wrong content")); err == nil {
t.Fatal("expected digest mismatch error, got nil")
}

// The incomplete file must be cleaned up after a digest mismatch.
if _, err := os.Stat(incompletePath(blobPath)); !errors.Is(err, os.ErrNotExist) {
t.Fatal("expected incomplete file to be removed after digest mismatch")
}
// The final blob must not exist.
if _, err := os.Stat(blobPath); !errors.Is(err, os.ErrNotExist) {
t.Fatal("expected final blob file not to exist after digest mismatch")
}
})

t.Run("WriteBlob reuses existing blob", func(t *testing.T) {
// Use the correct hash for "some-data" so digest verification passes.
hash, _, err := oci.SHA256(bytes.NewReader([]byte("some-data")))
if err != nil {
t.Fatalf("error computing hash: %v", err)
}

if err := store.WriteBlob(hash, bytes.NewReader([]byte("some-data"))); err != nil {
t.Fatalf("error writing blob: %v", err)
Expand Down
3 changes: 2 additions & 1 deletion pkg/inference/backends/diffusers/diffusers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/docker/model-runner/pkg/internal/dockerhub"
"github.com/docker/model-runner/pkg/internal/utils"
"github.com/docker/model-runner/pkg/logging"
"github.com/docker/model-runner/pkg/sandbox"
)

const (
Expand Down Expand Up @@ -255,7 +256,7 @@ func (d *diffusers) Run(ctx context.Context, socket, model string, modelRef stri
Socket: socket,
BinaryPath: d.pythonPath,
SandboxPath: "",
SandboxConfig: "",
SandboxConfig: sandbox.ConfigurationPython,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The SandboxPath is currently empty. On Darwin, the sandbox profile uses [UPDATEDBINPATH] and [UPDATEDLIBPATH] to grant access to the Python environment. [UPDATEDLIBPATH] is calculated as filepath.Join(filepath.Dir(updatedBinPath), "lib"). If SandboxPath is empty, the process will likely fail to load its bundled libraries. It should be set to the bin directory of the installation.

Suggested change
SandboxConfig: sandbox.ConfigurationPython,
SandboxPath: filepath.Join(d.installDir, "bin"),

Args: args,
Logger: d.log,
ServerLogWriter: logging.NewWriter(d.serverLog),
Expand Down
3 changes: 2 additions & 1 deletion pkg/inference/backends/mlx/mlx.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/docker/model-runner/pkg/inference/models"
"github.com/docker/model-runner/pkg/inference/platform"
"github.com/docker/model-runner/pkg/logging"
"github.com/docker/model-runner/pkg/sandbox"
Comment on lines 15 to +16
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

To correctly support sandboxing for custom Python paths on Darwin, we need to determine the directory of the Python binary to set the SandboxPath. This requires the path/filepath package.

Suggested change
"github.com/docker/model-runner/pkg/logging"
"github.com/docker/model-runner/pkg/sandbox"
"github.com/docker/model-runner/pkg/logging"
"github.com/docker/model-runner/pkg/sandbox"
"path/filepath"
)

)

const (
Expand Down Expand Up @@ -140,7 +141,7 @@ func (m *mlx) Run(ctx context.Context, socket, model string, modelRef string, mo
Socket: socket,
BinaryPath: m.pythonPath,
SandboxPath: "",
SandboxConfig: "",
SandboxConfig: sandbox.ConfigurationPython,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The SandboxPath should be set to the directory containing the Python binary. This ensures that the Darwin sandbox correctly calculates [UPDATEDLIBPATH] (as the sibling lib directory), allowing the Python process to load its dependencies. This is especially important when a customPythonPath is used.

Suggested change
SandboxConfig: sandbox.ConfigurationPython,
SandboxPath: filepath.Dir(m.pythonPath),

Args: args,
Logger: m.log,
ServerLogWriter: logging.NewWriter(m.serverLog),
Expand Down
3 changes: 2 additions & 1 deletion pkg/inference/backends/sglang/sglang.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/docker/model-runner/pkg/inference/models"
"github.com/docker/model-runner/pkg/inference/platform"
"github.com/docker/model-runner/pkg/logging"
"github.com/docker/model-runner/pkg/sandbox"
)

const (
Expand Down Expand Up @@ -169,7 +170,7 @@ func (s *sglang) Run(ctx context.Context, socket, model string, modelRef string,
Socket: socket,
BinaryPath: s.pythonPath,
SandboxPath: sandboxPath,
SandboxConfig: "",
SandboxConfig: sandbox.ConfigurationPython,
Args: args,
Logger: s.log,
ServerLogWriter: logging.NewWriter(s.serverLog),
Expand Down
3 changes: 2 additions & 1 deletion pkg/inference/backends/vllm/vllm.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/docker/model-runner/pkg/inference/models"
"github.com/docker/model-runner/pkg/inference/platform"
"github.com/docker/model-runner/pkg/logging"
"github.com/docker/model-runner/pkg/sandbox"
)

const (
Expand Down Expand Up @@ -180,7 +181,7 @@ func (v *vLLM) Run(ctx context.Context, socket, model string, modelRef string, m
Socket: socket,
BinaryPath: v.binaryPath(),
SandboxPath: vllmDir,
SandboxConfig: "",
SandboxConfig: sandbox.ConfigurationPython,
Args: args,
Logger: v.log,
ServerLogWriter: logging.NewWriter(v.serverLog),
Expand Down
5 changes: 3 additions & 2 deletions pkg/inference/backends/vllm/vllm_metal.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/docker/model-runner/pkg/internal/dockerhub"
"github.com/docker/model-runner/pkg/internal/utils"
"github.com/docker/model-runner/pkg/logging"
"github.com/docker/model-runner/pkg/sandbox"
)

const (
Expand Down Expand Up @@ -216,8 +217,8 @@ func (v *vllmMetal) Run(ctx context.Context, socket, model string, modelRef stri
BackendName: "vllm-metal",
Socket: socket,
BinaryPath: v.pythonPath,
SandboxPath: "",
SandboxConfig: "",
SandboxPath: v.installDir,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The SandboxPath is set to v.installDir, which is the root of the Python environment. However, the Darwin sandbox profile (see pkg/sandbox/sandbox_darwin.go:224) calculates [UPDATEDLIBPATH] by taking the parent of the provided path and appending lib. If SandboxPath is the root, UPDATEDLIBPATH will point to the parent directory of the environment instead of the environment's own lib directory, causing the backend to fail to load its libraries. It should be set to the bin directory.

Suggested change
SandboxPath: v.installDir,
SandboxPath: filepath.Join(v.installDir, "bin"),

SandboxConfig: sandbox.ConfigurationPython,
Args: args,
Logger: v.log,
ServerLogWriter: logging.NewWriter(v.serverLog),
Expand Down
70 changes: 70 additions & 0 deletions pkg/sandbox/sandbox_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,76 @@ import (
"strings"
)

// ConfigurationPython is the sandbox configuration for Python-based inference
// backends (vLLM, vllm-metal, SGLang, MLX). It mirrors the llama.cpp profile
// but additionally allows TCP loopback binding (used by vllm-metal and SGLang)
// and read access to the Python runtime install directory supplied via
// [UPDATEDBINPATH].
const ConfigurationPython = `(version 1)

;;; Keep a default allow policy (because encoding things like DYLD support and
;;; device access is quite difficult), but deny critical exploitation targets.
;;; Python backends run with the same constraint philosophy as llama.cpp.
(allow default)

;;; Deny network access except for our IPC sockets.
;;; Python backends use either a Unix socket or a TCP loopback port.
;;; Allow Unix socket paths that match the inference socket naming convention
;;; as well as TCP loopback binding/inbound for backends that use TCP.
(deny network*)
(allow network-bind network-inbound
(regex #"inference.*-[0-9]+\.sock$")
(local tcp "localhost:*"))

;;; 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)

;;; Deny access to launchservicesd to prevent sandbox escape via open(1).
(deny mach-lookup
(global-name "com.apple.launchservicesd")
(global-name "com.apple.coreservices.launchservicesd"))

;;; 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.
(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 "[UPDATEDBINPATH]")
(subpath "[UPDATEDLIBPATH]"))
(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]"))
`

// ConfigurationLlamaCpp is the sandbox configuration for llama.cpp processes.
const ConfigurationLlamaCpp = `(version 1)

Expand Down
4 changes: 4 additions & 0 deletions pkg/sandbox/sandbox_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"os/exec"
)

// ConfigurationPython is the sandbox configuration for Python-based inference
// backends. On non-Darwin platforms no sandboxing is applied.
const ConfigurationPython = ``

// ConfigurationLlamaCpp is the sandbox configuration for llama.cpp processes.
const ConfigurationLlamaCpp = ``

Expand Down
14 changes: 14 additions & 0 deletions pkg/sandbox/sandbox_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ var limitTokenToGenerator = map[string]func() winjob.Limit{
"(WithWriteClipboardLimit)": winjob.WithWriteClipboardLimit,
}

// ConfigurationPython is the sandbox configuration for Python-based inference
// backends (vLLM, SGLang, MLX) on Windows.
const ConfigurationPython = `(WithDesktopLimit)
(WithDieOnUnhandledException)
(WithDisplaySettingsLimit)
(WithExitWindowsLimit)
(WithGlobalAtomsLimit)
(WithHandlesLimit)
(WithDisableOutgoingNetworking)
(WithReadClipboardLimit)
(WithSystemParametersLimit)
(WithWriteClipboardLimit)
`

// ConfigurationLlamaCpp is the sandbox configuration for llama.cpp processes.
const ConfigurationLlamaCpp = `(WithDesktopLimit)
(WithDieOnUnhandledException)
Expand Down
Loading