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
:checksumis 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.
Same as validate_path/2 but raises MLServe.Error on failure.
Verifies a file against an expected digest.
Types
@type checksum() :: {:sha256 | :sha512, String.t()}
A digest algorithm and its expected lowercase hex value.
Functions
@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.
@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.
@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_rootopts::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}}
Same as validate_path/2 but raises MLServe.Error on failure.
@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 filechecksum:{:sha256 | :sha512, hex_string}, ornilto skip
Examples
iex> MLServe.Security.verify_checksum("/nonexistent", nil)
:ok