Skip to content

Repository files navigation

modelvet

CI status Latest release PyPI version MIT license

modelvet answers one question before a model loader touches a file: is this GGUF or safetensors file structurally safe to load?

It is a freestanding, strict C11 library with no heap allocation. Every hostile length, count, offset and tensor size is validated with checked arithmetic, in fixed memory, before any loader parses the bytes. Use it as a static library, as one vendored .c/.h pair, as a CLI in a pipeline, or from Python.

modelvet accepting two valid model files, rejecting a CVE-shaped file with exit code 1, and printing a JSON report

Quick start

The CLI. Exit code 0 is a verified ACCEPT, 1 a verified REJECT, 2 means no verdict:

git clone https://github.com/tetsuo-ai/modelvet
cd modelvet && make cli
./build/modelvet model.gguf
./build/modelvet --json suspicious.safetensors | jq .violation_name

Python:

pip install modelvet
import modelvet

report = modelvet.verify_path("model.safetensors")
print(report.accepted, report.violation_name)

C, vendored. make amalgamation emits build/modelvet.c and build/modelvet.h: copy both into your tree and compile as C11, no dependencies. The integration guide covers arena sizing, cap overrides and pipeline use.

Why this exists

llama.cpp (C++), Ollama (Go) and MLX (C++) each wrote their own GGUF reader, and each shipped the same class of integer overflow CVE. The 2025 llama.cpp fix was bypassed in 2026 because the patch checked each addition but not the final combined allocation. A format that everyone reimplements needs a check that runs before any loader parses the bytes. modelvet is that check.

A concrete example. A 65-byte GGUF whose tensor is shaped [4, 0] reaches INT64_MAX / info.t.ne[1] in llama.cpp's own overflow guard, where the dimension came from the file and only negatives were rejected. On x86-64 that divide by zero is a SIGFPE, and the loader dies before it reads a weight (llama.cpp issue #26366, open). modelvet rejects the same file with a violation code and a byte offset.

The llama.cpp loader taking a SIGFPE on a 65-byte crafted GGUF, then modelvet rejecting the same file with a violation code

The attacker controls every input byte. For every input, modelvet is designed to:

  • stay within the input buffer and caller-owned arena;
  • use checked arithmetic for file-derived sizes, counts, and offsets;
  • keep every loop bounded by a named compile-time cap;
  • avoid recursion, heap allocation, and hidden mutable state;
  • return malformed files as MVET_OK with MVET_VERDICT_REJECT and the first precise violation.

An ACCEPT verdict is structural. It says nothing about model behavior, provenance, poisoned weights, tokenizer semantics, or bugs a downstream loader introduces after validation. See the threat model for the complete boundary.

Evidence

  • CVE regression corpus: one file per advisory in tests/corpus/, each rejected with its exact violation code as a required CI gate. Root cause and enforcing invariant per advisory are in the CVE notes.
  • GGUF differential parity: modelvet and a real upstream GGUF loader run over the same inputs, and every divergence is triaged in the parity report. Across 249 inputs, none landed in "we accept / they reject", so modelvet is never more permissive than the loader it protects.
  • safetensors differential parity: the same harness linked against the canonical Rust implementation at the audited commit. Zero more-permissive inputs, zero canonical panics, and each divergence maps to a documented policy code. Details, including the recovered Trail of Bits polyglot fixtures, are in the safetensors parity report.
  • Fixed memory: the whole corpus plus both worst-case carve forcers run through a single 64 KiB arena under the documented reduced-cap profile (make check-arena-64k).
  • Bounded work: every input-scaling loop is counted and checked against a linear plus n log n budget, so hash flooding and quadratic scans fail the build (make check-work).

Using the C library

#include <stdint.h>

#include "modelvet.h"

uint8_t memory[MVET_GGUF_ARENA_WORST_BYTES];
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, file_bytes, file_length) != MVET_OK)
    return 1;
if (report.verdict != MVET_VERDICT_ACCEPT)
    return 1;

MVET_GGUF_ARENA_WORST_BYTES is an exact bound for the configured caps, including worst-case initial alignment padding. With the defaults it is 147,463 bytes on a 64-bit target. The documented 64 KiB profile lowers MVET_MAX_TENSORS to 2048 and requires 49,159 bytes.

mvet_st_verify follows the same contract for safetensors files with the exact bound MVET_ST_ARENA_WORST_BYTES (278,551 bytes at 64-bit defaults; its documented 64 KiB profile needs 57,367). One arena sized to the larger bound serves both verifiers; small inputs carve proportionally less.

Violation-code numbers are append-only ABI: never renumbered, never reused. Persist them freely. docs/RELEASE.md carries the full versioning policy.

Command line and bindings

make cli builds modelvet(1): one file per invocation, exit code = verdict, --json single-object reports for pipelines, and content-based format routing. See the man page at docs/modelvet.1.

$ build/modelvet --json model.gguf
{"file":"model.gguf","format":"gguf","verdict":"accept","violation":0,...}

A ctypes Python binding lives in bindings/python (verify_gguf, verify_safetensors, verify_path). The integration guide covers vendoring the two-file amalgamation, arena sizing, pipeline usage, and what an ACCEPT verdict does not mean.

Building and checking

make lib                  # build build/libmodelvet.a
make test                 # run the hosted unit and behavior tests
make check                # tests plus every repository gate below
make fuzz                 # build the libFuzzer + ASan targets
make amalgamation         # emit and behavior-test build/modelvet.{c,h}
make check-amalg          # full test sweep + CVE corpus against the amalgamation
make cli                  # build the modelvet(1) binary (build/modelvet)
make check-cli            # CLI exit-code and JSON contract test
make test-profile-64k     # verify the documented 64 KiB configuration
make test-no-assert       # prove release behavior without internal assertions
make check-corpus         # reject every CVE corpus file with its exact code
make check-arena-64k      # whole corpus through one fixed 64 KiB arena
make check-work           # bounded-work proof against a closed-form budget
make shared check-python  # shared library + Python binding contract
make diff-st              # safetensors differential vs the pinned canonical crate
make dist                 # reproducible release tarball with SHA-256

Both GCC and Clang are supported. Compiler and profile flags are ordinary Make variables, for example:

make clean && make test CC=clang
make clean && make test CC=clang \
  EXTRA_CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all"

The checked-in GGML dtype table is pinned to the llama.cpp revision in tools/ggml-pin.env. Given a checkout of that source, verify it with:

make check-ggml GGML_DIR=/path/to/llama.cpp

The safetensors contract (frame checks, offset tiling, dtype bit widths, whitespace policy) mirrors the canonical Rust implementation pinned in tools/st-pin.env. Deliberate strictness beyond canonical behavior (duplicate keys, escapes in keys, unknown tensor fields, and the caps) is marked "policy" on its violation codes in the public header.

Scope

modelvet performs structural verification only. It intentionally does not parse pickle/PyTorch payloads, inspect model behavior, validate tokenizer semantics, provide cryptographic signing, or run inference.

The repository's engineering guide defines the C discipline, architecture, invariants, testing matrix, and upstream drift process that support the security contract.

Status

Pre-release, version 0.1.0. GGUF v2/v3 and safetensors structural verification are implemented, with a modelvet(1) CLI and a Python binding; the public API is not yet stable. Violation-code numbers are already append-only ABI (see docs/RELEASE.md). Do not use this release as a production security boundary without an independent review.

License

MIT. Copyright (c) 2026 AgenC (tetsuo-ai). Built and maintained by the AgenC team (tetsuo-ai).

About

Structural safety validator for ML model container files (GGUF, safetensors)

Resources

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages