Integrating modelvet
August 4, 2026 ยท View on GitHub
modelvet is a verify-before-load boundary: it decides whether a GGUF or
safetensors file is structurally safe to hand to a model loader, in
fixed memory, before any loader touches the bytes. This guide covers the
four ways to consume it (vendoring the two-file amalgamation, linking
the static library, running the modelvet(1) CLI in a pipeline, and the
Python binding) plus the arena sizing table and the exact limits of an
ACCEPT verdict.
What ACCEPT does not mean
Read this first; it is the honest boundary of the tool.
ACCEPT means the file's structure was verified: every length, count, offset, and size was checked against bounded arithmetic and the format's rules, so a conforming loader will not be driven out of bounds by this file's structure. ACCEPT does not mean:
- the model's behavior is safe, aligned, or unpoisoned;
- the weights or tokenizer do what the publisher claims;
- the file's provenance is trusted or its contents signed;
- every loader will accept the file (some loaders enforce their own, sometimes tighter, limits, e.g. llama.cpp's 64-byte tensor-name buffer against modelvet's default 256-byte cap);
- a loader with bugs after structural parsing is protected.
REJECT is precise: the report carries the first violation's code, byte offset, and two per-code detail values. A REJECT is a structural fact about the file, not a malware verdict.
Pickle-based formats (PyTorch .pt/.pkl) are out of scope by design
and cannot be made structurally safe; do not route them here expecting a
meaningful verdict.
Vendoring the amalgamation (recommended)
make amalgamation emits build/modelvet.c and build/modelvet.h:
one source file, one header, no dependencies beyond a freestanding C11
toolchain. Copy both files into your tree and compile the .c like any
other file:
cc -std=c11 -Wall -Wextra -Werror -c modelvet.c
Properties your build can rely on:
- Freestanding: no allocation, no stdio, no errno, no filesystem calls, no writable global state. The library writes only to the caller's arena, the report, and output parameters.
- The pair is byte-stable: regenerating from the same source tree produces identical bytes, so vendored copies diff cleanly against releases.
-ffreestandingis supported but not required.
Release tarballs (modelvet-X.Y.Z.tar.gz, see
RELEASE.md) ship the same pair plus this guide, with a
SHA-256 checksum and a detached signature.
Linking the static library
make lib # build/libmodelvet.a
cc -std=c11 -Iinclude your_scanner.c build/libmodelvet.a
Same code, same guarantees; use this when your build prefers archives over vendored sources.
Calling the API
#include <stdint.h>
#include "modelvet.h"
static uint8_t memory[MVET_GGUF_ARENA_WORST_BYTES > MVET_ST_ARENA_WORST_BYTES
? MVET_GGUF_ARENA_WORST_BYTES
: MVET_ST_ARENA_WORST_BYTES];
int scan(const uint8_t *bytes, size_t length)
{
mvet_arena_t arena = {0};
mvet_report_t report = {0};
if (mvet_arena_bind(&arena, memory, sizeof(memory)) != MVET_OK)
return -1;
if (mvet_gguf_verify(&report, &arena, bytes, length) != MVET_OK)
return -1; /* API misuse or arena too small: no verdict */
return report.verdict == MVET_VERDICT_ACCEPT ? 0 : 1;
}
Contract points that matter to integrators:
- A malformed file is not an error: the call returns
MVET_OKand the report saysMVET_VERDICT_REJECTwith the first violation.MVET_ERR_*means your code misused the API or under-sized the arena; treat it as "not verified", never as a verdict. - The report is fail-closed: it is cleared to all-zero (REJECT, no violation) before anything else happens.
- One arena serves both verifiers; every call resets it. The library never touches memory outside the arena, the report, and the input.
- Thread safety: no global state, so concurrent calls are safe as long as each thread uses its own arena and report.
- Violation-code numbers are append-only ABI; persist them freely (RELEASE.md carries the full policy).
Arena sizing
The bounds are exact closed forms over the compile-time caps, including worst-case alignment padding; the macros compute them for whatever cap overrides you build with. At the defaults on a 64-bit target:
| Configuration | GGUF | safetensors |
|---|---|---|
| Default caps | 147,463 B (MVET_GGUF_ARENA_WORST_BYTES) | 278,551 B (MVET_ST_ARENA_WORST_BYTES) |
| Documented 64 KiB profile | 49,159 B (-DMVET_MAX_TENSORS=2048u) | 57,367 B (-DMVET_MAX_ST_TENSORS=1536u -DMVET_MAX_ST_METADATA_ENTRIES=512u) |
One arena sized to the larger bound serves both formats. Small inputs
carve proportionally less; the bound is the worst case, not the typical
demand. Every cap is an #ifndef-overridable MVET_MAX_* define;
raising a cap only raises the bound, never the API. The reduced-cap
64 KiB profiles are CI-proven against the whole CVE corpus
(make check-arena-64k).
The CLI in pipelines
make cli builds build/modelvet. One file per invocation; the exit
code is the verdict:
| Exit | Meaning |
|---|---|
| 0 | verified ACCEPT |
| 1 | verified REJECT |
| 2 | usage, I/O, or API error. No verdict; never treat as ACCEPT |
modelvet --json downloaded.gguf
emits one JSON object with fixed field order:
{"file":"downloaded.gguf","format":"gguf","verdict":"accept",
"violation":0,"violation_name":"MVET_V_NONE","offset":0,
"detail":[0,0],"version":"0.1.0"}
offset and detail are unsigned 64-bit; parse with a 64-bit-capable
JSON reader if you need exact values above . Fields are append-only
across releases. See the man page (docs/modelvet.1) for the full
contract.
Format detection
--format auto (the default) routes by content, not extension: a file
whose first four bytes are the GGUF magic (GGUF) goes to the GGUF
verifier; everything else goes to the safetensors verifier. The rule
cannot misroute a valid file of either format: safetensors has no magic
of its own, and a safetensors file that starts with the GGUF magic
would declare a header length whose low 32 bits are 0x46554747
(over 1.1 GB), far past the canonical 100 MB header cap, so it is
invalid regardless of routing. Force --format gguf|safetensors when
your pipeline already knows the container type.
The same pair of checks (the 4-byte magic for GGUF; for safetensors, a
plausible little-endian u64 header length within the cap and within the
file, with the first header byte being { or JSON whitespace) is a
sound recognition heuristic for scanners that must type files without
extensions. Recognition is cheap triage only; a verdict still requires
running the full verifier.
Python (hub-side pipelines)
bindings/python/ wraps the C library with ctypes:
import modelvet
report = modelvet.verify_path("model.safetensors") # auto-routes
if not report.accepted:
print(report.violation_name, hex(report.offset), report.details)
verify_gguf(data) and verify_safetensors(data) take bytes-like
objects; verify_path maps the file and routes with the magic rule. API
misuse and arena exhaustion raise exceptions; a malformed file returns a
report, mirroring the C contract. Point MODELVET_LIBRARY at a specific
shared library (make shared builds build/libmodelvet.so), or install
the package, which builds the native module from the vendored
amalgamation.
Antivirus / file-type scanners
For engines that scan by file type (ClamAV's CL_TYPE_AI_MODEL slot is
the shape this was designed for): call the verifier from the type's
handler with a windowed read of the whole object, report REJECT with the
violation code as the detection detail, and treat exit-2-class API
failures as scan errors rather than detections. The library's
whole-buffer entry maps directly onto fmap-style access; memory is fixed
and bounded by the table above.