Security
Supported versions, how to report a vulnerability, what the urna hashes prove, how to treat a downloaded .urna file, and where network access exists.
This page covers the supported versions, how to report a vulnerability, what the integrity values in a .urna file prove, how the reader treats a file you did not build, and every place where urna opens a network connection.
Supported versions
Fixes land on the latest minor release line only.
| Version | Status |
|---|---|
| 0.5.x | Supported (current: 0.5.1) |
| 0.3.x and earlier | Not supported, upgrade to 0.5.x |
There is no 0.4.0 release: that version was never tagged, and its changes first shipped in 0.5.0. See the changelog.
Report a vulnerability
Do not open a public GitHub issue for a security problem. Use one of these private channels:
- The private vulnerability report form:
https://github.com/hoffresearch/urna/security/advisories/new - Email:
brenner@hoffresearch.com
The maintainer aims to acknowledge a report within 72 hours and to publish a fix or mitigation within 14 days for confirmed reports. Coordinated disclosure is preferred, and reporters who ask for credit get it.
A useful report includes:
- The
file_hashandcontent_hashof the.urnainvolved (urna stats <file>prints both). - The
simd_backendand the platform (urna statsprints the backend). - The exact CLI or Python invocation.
- A minimal reproducer if you have one. A synthetic
.urnais fine; the fixtures undercrates/urna-format/tests/fixtures/in the repository are a starting point. - A proposed mitigation, if you have one.
In scope
- A malformed
.urnafile that makes the Rust runtime hit undefined behavior, read out of bounds, panic or abort. - A citation collision: two distinct chunks that produce the same
chunk_id. - A
content_hashcollision under the v1 hash domain separation. - A text query path (
search-text,ask,retrieve) that skips themodel_hashcheck without an explicit skip flag. The only skip flag isurna search-text --skip-model-hash-check.search-spacetakes a raw vector and checks the hash only when you pass--expect-model-hash. - A path that runs model-repository code (
trust_remote_codepresets) without the explicit opt-in, or that runs a pinned preset's code file whose SHA-256 is outside the pinned allowlist. See model code. - Secrets or credentials committed to the repository.
Out of scope
- Low recall on a particular corpus, or HNSW recall below your expectation. That is tuning; see search paths.
- The BM25 tokenizer on CJK, Thai and Lao text. It is a documented limitation, listed in known limits.
- Size differences between compressed and raw builds.
- Vulnerabilities in the upstream sentence-transformers or Hugging Face stack. Report those upstream first.
- Weaknesses of an embedding model itself, such as false positives or biased recall.
- Operator choices, such as building a corpus with the placeholder
model_hashand then querying it with--skip-model-hash-check.
What the hashes prove
A .urna file carries several integrity values. All of them are unkeyed SHA-256: they detect corruption, not tampering.
| Value | Width | Covers |
|---|---|---|
| Header checksum | 8 bytes (first 8 bytes of a SHA-256) | The 128-byte header with the checksum field cut out |
| Section checksum | 8 bytes (first 8 bytes of a SHA-256) | One section's payload bytes as stored on disk, padding excluded |
| Footer hash | 32 bytes | Every byte before the 40-byte footer |
file_hash | sha256:<64 hex> | The whole file, footer included. It equals sha256sum file.urna |
content_hash | sha256:<64 hex> | The six canonical sections after decoding, so it does not change with the text encoding |
The header and section checksums are 8-byte prefixes, not full sha256:<64 hex> strings. urna inspect --json prints a section checksum as 16 hex characters with no prefix. The footer hash and the reported file_hash are two different numbers: the footer excludes itself, the reported value covers every byte. The manifest is covered by the footer hash only, so editing a manifest field changes file_hash and leaves content_hash and every citation unchanged. The layouts and preimages are in hashes and citations and hashes.
"Verifiable" means integrity, not authenticity. Anyone who edits a file can recompute every value above, and the file carries no signature; the manifest authors field is free text. urna validate passing proves the bytes are internally consistent. It does not prove who built the file. To trust a corpus, get its file_hash from its publisher over a channel you already trust and compare it with urna stats or sha256sum.
Treat a downloaded .urna as untrusted input
Opening a file runs the parser on bytes you did not write. Safety against a hostile file rests on that parser's memory safety, so give an unknown .urna the same care as any untrusted input.
What the reader checks
When the runtime opens a file (MmapUrnaFile::open, used by ask, retrieve, every search verb and the Python urna.open), it checks, in order:
- The magic (
URNA, or the legacyNEST), the header version, the header checksum, and that the recorded file size equals the real size. - For each section: that its encoding is legal for its class, that its offset is 64-byte aligned and inside the file, and its checksum.
- That the manifest parses and passes its rules, then the footer hash.
- That the manifest and header agree on
embedding_dimandn_chunks, that all six required sections are present, that the embeddings section has the exact size its dtype implies, and that thesearch_contractsection matches the manifest field by field. - That no embedding value is NaN or infinite, and the same for each multimodal band and for a full-precision rerank slab when present.
- That the HNSW, BM25, graph and media payloads decode, with every count read from the file bounded by the remaining bytes before it sizes an allocation.
What the reader does not check
- Section ids it does not know. An unknown or reserved id loads when its encoding is legal for its class and its checksum matches.
- Duplicate section ids (the first match wins) and sections that overlap each other, the table or the manifest. Only bounds and alignment are checked.
- The header
flagsandreservedbytes, and a section table offset other than 128. - Whether a file is honest. The checks prove consistency; they cannot tell a crafted file from a real one.
urna validate runs a subset of the open path: the byte-level checks, the NaN walk over the main embeddings, the contract decode and a SHA-256 proof of every inlined media blob. It does not decode the HNSW, BM25, graph or overlay payloads and does not NaN-check the multimodal bands. A file with a malformed index payload and a valid checksum passes validate and fails to open. Both paths read every byte: validate loads the whole file into memory, and open maps it and hashes all of it.
Hardening in the parser
clippy::unwrap_usedandclippy::undocumented_unsafe_blocksare denied across the workspace (tests exempt from the first).urna-format, the crate that parses the container, has nounsafeblock, and CI runs its tests under miri on a nightly schedule.unsafeinurna-runtimeis limited to the SIMD kernels, theMmap::mapcall inmmap_file.rsand theposix_madvisecall inmmap_cold.rs. Every block carries a// SAFETY:comment, and the SIMD dispatchers check slice lengths withassert!, kept in release builds.- Header-derived sizes are overflow-checked, payload cursors check
need > remaining, and every score sort puts NaN last. - A deterministic mutation harness runs under
cargo test, fourcargo-fuzztargets run for 90 seconds each on every pull request and push tomain, and a nightly job soaks each target for 30 minutes. The first runs found and fixed five classes of malformed-file bug, including a count claim that asked the allocator for 31 GB from a 90-byte payload.
Queries run local Python
ask, retrieve and search-text start a Python process to embed the query, and doctor starts one for a test embed. The file does not supply that code, but where the code comes from depends on your working directory:
- The embedder script is looked up in a checkout layout first (
python/...under the current directory, then under its parent), and only then in the installed payload. A checkout in your working directory or its parent wins. - The interpreter is
URNA_PYTHON, else the venvurna setupcreated, else the nearest.venv/bin/pythonin the current directory or up to three parents, elsepython3. The choice is printed on stderr as[urna] embedder interpreter: <path>. --embedderonask,retrieveandsearch-textruns whatever script you name.
Run queries from a directory you trust, or pin URNA_PYTHON. The manifest's embedding_model picks between the potion script and the registry script; a registry preset that needs remote code still requires your opt-in.
urna media --export checks each inlined blob against its SHA-256 in blob_refs before writing it, and writes only the last component of the blob's URI, so a hostile URI cannot write outside the export directory.
Where urna opens a network connection
The Rust runtime never opens a socket, and the urna binary links no network stack. Queries are answered from the memory-mapped file. These are the places where a connection does happen:
| Where | What connects | Notes |
|---|---|---|
install.sh | curl | Downloads the archive, the embedder payload and their .sha256 files, and checks both digests before writing |
install.ps1 | Invoke-WebRequest | Same four files, checked with Get-FileHash |
npm @urna/cli | The package's Node wrapper | Downloads the release archive at install or on first run, with no checksum check |
urna setup, payload step | A system curl child process | https:// or file:// only, redirects to https:// only, SHA-256 checked while streaming |
urna setup, Python step | uv or pip | Installs numpy and tokenizers from your package index; uv may also download a Python interpreter when none is found |
| Python embedders and builders | Hugging Face Hub | Only with URNA_ALLOW_DOWNLOAD=1; otherwise they set HF_HUB_OFFLINE, TRANSFORMERS_OFFLINE and HF_DATASETS_OFFLINE to 1 when unset |
scripts/fetch_potion.sh (checkout only) | curl | Fetches the potion table at a pinned revision and checks its SHA-256 |
For an install with no network at all, see air-gapped install and queries. The design is explained in offline by construction.
Model code and remote code
Five registry presets need trust_remote_code: jina-v5-omni-nano, jina-v5-omni-small, wemm-2b, wemm-4b and wemm-9b. A build loads them only when the spec lists the preset in [output] allow_remote_code. The query, bench and UI-bridge scripts load them only when URNA_ALLOW_REMOTE_CODE names the preset (a comma list). A manifest cannot grant this by naming a model.
Only wemm-2b pins its remote code: five files with fixed SHA-256 values, and a changed or missing file is refused. jina-v5-omni-nano, jina-v5-omni-small, wemm-4b and wemm-9b have no pins, so the opt-in is their only gate. A pinned hash identifies a version; it does not make the code safe. Review the files before you trust a new pin, and build in an isolated environment when the model directory is not fully trusted. Each sentence-transformers model runs in its own worker process, which keeps models apart from each other and is not a sandbox. See choose and bring embedding models.
Release provenance
- Release tags are annotated and SSH-signed.
tag-verify.ymlchecks the signature against.github/allowed_signersbefore the release archives build. - The five binary archives carry a per-file
.sha256and a SLSA build provenance attestation (gh attestation verify <archive> --repo hoffresearch/urna). - The binaries embed their dependency tree (
cargo auditable), the release ships a CycloneDX SBOM (urna.cdx.xml), andCargo.lockis committed. cargo denychecks advisories, licenses and sources in CI.- The PyPI wheels for 0.5.0 and 0.5.1 carry no PEP 740 attestations, and the npm package carries registry signatures but no npm provenance.
The payload and the SBOM have no build provenance
The SLSA build attestation covers the five archives only. The embedder payload (urna-embedder-payload.tar.gz, which holds Python code that runs at query time) and the SBOM are covered by the GitHub release attestation alone, so gh attestation verify finds no attestation for them. Check them with gh release verify-asset v0.5.1 <file>, and the payload also against its .sha256. See known limits.
The per-artifact commands are in verify what you installed.
Known limits
Where urna 0.5.1 behaves differently from what you might expect, grouped by building, querying, file format and install, with a workaround for each.
Data governance
What a .urna file stores, what it leaves out, how to remove or correct content, where processing runs, and which files a build leaves on disk.