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¶
Declared backend per artifact — bundle metadata records the serialization backend for every artifact:
joblib,pickle,sklearn_estimator_dict,onnx,rds,torch_state_dict, etc.Default-deny on code-bearing backends —
dag-ml predict/ replay refuses to load apickle/joblib/ code-bearingrdsartifact unless the caller passes an explicit opt-in:--allow-pickle(CLI) orallow_unsafe_artifacts=True(Python). The default raisessecurity/unsafe_artifact_refused(ADR-11) with a remediation hint.Recommended code-free backends —
sklearn_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 preferssklearn_estimator_dictwhere the estimator supports round-tripping its fitted state as plain arrays.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.Bundle signing (deferred) — Sigstore/cosign signing is the intended hardening path but is deferred to a follow-up release (descoped). This ADR reserves a
signaturefield in the bundle schema now so adding signing later is non-breaking.
Consequences¶
The bundle schema gains
artifacts[].serialization_backend(required) and a reserved optionalsignaturefield.Workstream A’s bundle loader implements the default-deny gate; workstream E’s artifact bridge tags each persisted artifact with its backend.
SECURITY.mddocuments the artifact-loading trust model next to ADR-13 (the process-adapter half).
Risk¶
Default-deny breaks naïve “just
joblib.dumpeverything” workflows. This is intentional and mirrorsnumpy.load(allow_pickle=False)andtorch.load(weights_only=True)precedent. The error names the--allow-pickleflag and recommends migrating tosklearn_estimator_dict.