MLServe.Security (MLServe v0.1.0)

Copy Markdown View Source

Model artifact validation: path containment, size limits, and integrity checks.

MLServe treats a model file as data, never as code. The core library never calls :erlang.binary_to_term/1, Code.eval_string/1, or loads a NIF from a path supplied through configuration. Supplying a model file must not be a way to execute arbitrary code, so this module validates the path before any backend sees it.

Backend authors

These guarantees stop at MLServe's boundary. If your backend deserialises a model artifact, use :erlang.binary_to_term(bin, [:safe]) — the unsafe form can exhaust the atom table and construct arbitrary terms from a hostile file.

What is checked

  • The path resolves inside the configured :model_root.. traversal and absolute paths pointing elsewhere are rejected before the file is touched, and the check is repeated after symlink resolution so a link inside the root cannot escape it.
  • The file exists, is a regular file, and is readable.
  • The file is no larger than :max_model_bytes.
  • When a :checksum is configured, the file's digest matches.

Summary

Types

A digest algorithm and its expected lowercase hex value.

Functions

Computes the lowercase hex digest of a file, streaming it rather than reading it into memory.

Ensures a module is loaded and actually implements MLServe.Model.

Validates a configured model path and returns its absolute, symlink-resolved form.

Verifies a file against an expected digest.

Types

checksum()

@type checksum() :: {:sha256 | :sha512, String.t()}

A digest algorithm and its expected lowercase hex value.

Functions

digest(path, algorithm \\ :sha256)

@spec digest(String.t(), :sha256 | :sha512) ::
  {:ok, String.t()} | {:error, {:invalid_path, atom()}}

Computes the lowercase hex digest of a file, streaming it rather than reading it into memory.

Model artifacts are routinely gigabytes; File.read!/1 followed by :crypto.hash/2 would double peak memory at load time for no reason.

validate_backend(backend)

@spec validate_backend(module()) ::
  :ok | {:error, {:invalid_backend, :not_loaded | :not_a_model}}

Ensures a module is loaded and actually implements MLServe.Model.

Checked at load time rather than assumed. A typo'd backend module would otherwise surface as an UndefinedFunctionError on the first prediction, long after the misconfiguration.

validate_path(path, opts \\ [])

@spec validate_path(
  String.t(),
  keyword()
) ::
  {:ok, String.t()}
  | {:error, {:invalid_path, atom()} | {:checksum_mismatch, String.t()}}

Validates a configured model path and returns its absolute, symlink-resolved form.

Parameters

  • path: the configured path, absolute or relative to :model_root
  • opts: :root (defaults to the configured model root), :max_bytes, :checksum

Examples

iex> MLServe.Security.validate_path("../../etc/passwd", root: "/srv/models")
{:error, {:invalid_path, :outside_root}}

validate_path!(path, opts \\ [])

@spec validate_path!(
  String.t(),
  keyword()
) :: String.t()

Same as validate_path/2 but raises MLServe.Error on failure.

verify_checksum(path, arg2)

@spec verify_checksum(String.t(), checksum() | nil) ::
  :ok | {:error, {:checksum_mismatch, String.t()} | {:invalid_path, atom()}}

Verifies a file against an expected digest.

Parameters

  • path: absolute path to the file
  • checksum: {:sha256 | :sha512, hex_string}, or nil to skip

Examples

iex> MLServe.Security.verify_checksum("/nonexistent", nil)
:ok