ADR-16: Artifact serialization security

Status: accepted (2026-05-29) Blocks: workstream A (bundle), workstream E (bridge)

Context

Fitted models are persisted as artifacts inside .n4a bundles. The common Python serializers — pickle and joblib — execute arbitrary code on load. Loading an untrusted bundle is therefore a remote-code-execution vector. Replay and predict load artifacts by definition, so this is on the hot path.

Decision

  1. Declared backend per artifact — bundle metadata records the serialization backend for every artifact: joblib, pickle, sklearn_estimator_dict, onnx, rds, torch_state_dict, etc.

  2. Default-deny on code-bearing backendsdag-ml predict / replay refuses to load a pickle / joblib / code-bearing rds artifact unless the caller passes an explicit opt-in: --allow-pickle (CLI) or allow_unsafe_artifacts=True (Python). The default raises security/unsafe_artifact_refused (ADR-11) with a remediation hint.

  3. Recommended code-free backendssklearn_estimator_dict (params + fitted arrays, no pickled callables), onnx, and framework-native state dicts are safe to load without the flag. The sklearn process adapter prefers sklearn_estimator_dict where the estimator supports round-tripping its fitted state as plain arrays.

  4. Dual validation — the loader checks both the per-artifact content fingerprint (already in bundle.rs) and the declared backend before loading. A backend that doesn’t match the file’s detected format is refused.

  5. Bundle signing (deferred) — Sigstore/cosign signing is the intended hardening path but is deferred to a follow-up release (descoped). This ADR reserves a signature field in the bundle schema now so adding signing later is non-breaking.

Consequences

  • The bundle schema gains artifacts[].serialization_backend (required) and a reserved optional signature field.

  • Workstream A’s bundle loader implements the default-deny gate; workstream E’s artifact bridge tags each persisted artifact with its backend.

  • SECURITY.md documents the artifact-loading trust model next to ADR-13 (the process-adapter half).

Risk

  • Default-deny breaks naïve “just joblib.dump everything” workflows. This is intentional and mirrors numpy.load(allow_pickle=False) and torch.load(weights_only=True) precedent. The error names the --allow-pickle flag and recommends migrating to sklearn_estimator_dict.