Audit remediation: host build, public API + Law I model binding, BNN/MLP correctness, engine tests#1
Open
anjaustin wants to merge 9 commits into
Open
Conversation
The documented `cmake && make && ctest` flow never compiled:
- target_include_directories only listed `include src`, so the first
engine TU failed on `#include "mnv_blake2s.h"` (lives in src/security).
- MNV_SOURCES omitted mnv_lut.c and mnv_outauth.c, which the engine
and the host test both reference -> link failure.
Add every src/ subdir to the include path and the two missing TUs to the
library. This is also the first time the engine/arch sources compile under
CMake; fix the one -Wall -Wextra warning that surfaced (signed/unsigned
compare in mnv_blake2s_verify).
Red-team: `cmake -S . -B build -DMNV_TARGET=host && cmake --build build`
is now warning-free and `ctest` passes 32/32.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two defects fixed together: 1. API/contract breakage. The working entry point mnv_run_with_model() was never declared in minerva.h, so the README quick-start and the shipped example failed to compile (call to undeclared function). The declared mnv_run() was a dead shim returning MNV_ERR_CONFIG, and mnv_verify_output() was declared but never defined (link error if called). 2. Verification bypass (Law I). mnv_run_with_model() accepted a model pointer at run time but only checked ctx->verified, which mnv_init() set for whatever model it was given. So mnv_init(ctx, trusted) followed by mnv_run_with_model(ctx, attacker_model, ...) executed UNVERIFIED weights. Fix: mnv_init() now binds the verified model into ctx->model. mnv_run() and mnv_verify_output() are real implementations operating on that bound model. mnv_run_with_model()/mnv_verify()/mnv_verify_output_with_key() remain as explicit-model/explicit-key variants but reject any model pointer that differs from the one bound at init (MNV_ERR_CONFIG). All four are now declared in minerva.h. Red-team (ASan+UBSan): a TU including only minerva.h compiles and links against all four entry points; a real ChaCha20+BLAKE2s model runs end to end via mnv_run(); running/verifying a different model object is rejected with CONFIG; a flipped ciphertext byte is still caught as TAMPER. 9/9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Until now the host suite exercised only crypto/primitive functions; the inference engine (mnv_init + forward passes) had ZERO coverage, which is how the API/bypass and arch bugs shipped green. Add tests that build a real ChaCha20-encrypted, BLAKE2s-MAC'd model in memory and run the full mnv_init() + mnv_run() pipeline, comparing the output byte-for-byte against an INDEPENDENT integer reference (not the engine's own arithmetic), plus tamper / null / bad-ABI negative paths. A bash driver compiles one binary per arch (the engine is monomorphized at compile time) under ASan + UBSan, wired into ctest as minerva_engine_tests. MLP (8->16->8->4) and CNN1D both match their references exactly. The driver is also where the wide-hidden-layer MLP repro for the scratch-sizing bug will live (added with its fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three coupled defects made the Binary Neural Network arch unusable beyond a single layer: 1. mnv_config.h auto-defined MNV_QUANT_Q8 whenever it was absent, so a -DMNV_QUANT_BINARY (or Q4/Q15) request was silently overridden by Q8. BINARY was only reachable via the ATtiny FORCE_BINARY path. Default to Q8 only when no quantization is selected (mirrors the arch-default guard). 2. mnv_bnn_forward() decrypted from model->encrypted_weights (offset 0) on every layer. The ChaCha keystream advances internally but the ciphertext pointer did not, so layer >0 XOR'd the wrong bytes. Track ct_off like the MLP/CNN paths. 3. The engine never consumed the per-layer bias bytes the compiler emits, so even with an offset fix the stream desynced after layer 0. Decrypt the int8 biases ([packed W][int8 b] per layer) and add them at Q8 scale before the activation -- also giving BNN the biases real binary nets need. Red-team: new reference-checked multi-layer BNN engine test (8->8->8->4, widths multiple of 8). Passes with the fix; on the reverted engine it fails with concrete wrong output (engine[-63 0 0 -31] vs ref[65 3 0 -1]). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The activation buffers (buf_a/buf_b via MNV_CTX_BUF_SIZE), the weight decryption scratch, and the local bias scratch were all sized assuming layer 0 is the widest layer (MNV_LAYER_0_SIZE * MNV_INPUT_SIZE, etc.). When a hidden or output layer is wider, decryption and activation writes overflow the buffers -- ASan: stack-buffer-overflow in mnv_chacha20_decrypt via bias_scratch. The shipped configs only avoided it because they happened to set LAYER_1_SIZE == INPUT_SIZE. Add integer-constant-expression max macros MNV_MAX_LAYER_WEIGHTS (widest weight matrix) and MNV_MAX_ACT_WIDTH (widest activation across input, hidden, and output) and size every per-layer buffer to them: weight_scratch, buf_a/buf_b, mnv_mlp.c bias_scratch, and the BNN packed buffers. Tighten the SRAM static_assert to MNV_MAX_ACT_WIDTH. Red-team: added wide-hidden-layer MLP (4->8->16->4) and BNN (8->8->16->8) configs to the engine test driver. Both ASan-crashed before this change and now match their independent references. Full ctest green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When in_sz % 8 != 0, a neuron's weights are not byte-aligned in the packed buffer, and the last packed byte mixes real weights with the next neuron's bits / zero padding. The old byte-wise popcount path extracted whole bytes per neuron (mis-aligned) and counted padding/neighbor bits as agreements, with disagrees = n_in_byte - agrees underflowing. Aligned widths happened to work; sub-byte widths produced garbage. Replace it with a bit-addressed XNOR accumulator that reads each value's weight bit directly at n*in_sz + i and each activation bit at i: exactly in_sz iterations, no padding, any alignment. Kept branchless so timing stays data-independent (Law II). Removes the per-neuron memcpy and the packed_weights scratch buffer. Red-team: new sub-byte BNN config (5->6->5->3) in the engine driver. Passes with the fix; on the old byte-popcount code it fails with concrete wrong output (engine[126 126 78] vs ref[-77 24 -74]) while aligned widths still pass. Full ctest green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mnv_ct_confidence_check() compared (uint8_t)max_val against the threshold, so a negative maximum logit (e.g. -1 -> 255) passed any MNV_MIN_CONFIDENCE. Use a signed comparison: a negative max logit is low confidence and is rejected. The MNV_MIN_CONFIDENCE==0 "disabled" short-circuit is kept so the default still accepts all-negative outputs. Also decouple the check from input validation: the engine gated the confidence call on MNV_ENABLE_INPUT_VALIDATION. Add MNV_ENABLE_CONFIDENCE_CHECK and gate on that. Red-team: new test_confidence.c built with MNV_MIN_CONFIDENCE=20. The negative-max case returns OK on the old code (the bug) and CONFIDENCE with the fix; the other cases are unchanged. Full ctest green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README and the BibTeX citation claim an MIT license and reference a LICENSE file that did not exist; add it. (Copyright line is generic; replace with a specific holder as desired.) Add a .gitignore so a device key or generated model is not committed by accident: the repo previously had none despite secrets.h / key.bin being flagged "never commit". Covers build artifacts, in-tree test binaries, generated weights.c/weights.h/weights_debug.npz, and secrets (secrets.h, key.bin, *.key, *.pem). Verified that demo_model.npz and tracked sources are not ignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Version drift: minerva.h banner (was 1.1.0) and README BibTeX (was 1.2.0) -> 1.3.0, matching MNV_VERSION_STR. - mnv_init() doc: drop the false "ChaCha20 keystream precomputation" claim; note it binds the verified model (keystream is derived per layer in run). - mnv_crypto_header_t.mac comment: "over plaintext" -> over ciphertext (encrypt-then-MAC), matching the implementation and threat model. - Blinded LUT comment: describe the access pattern accurately (fixed order, random rotation) instead of "different order each run". - Note the file-scope scratch buffers in cnn1d/bnn make those paths single-context / not reentrant, and are not wiped by mnv_destroy(). - Test badge no longer claims a stale count; README test section documents the working CMake/ctest flow and the engine-test driver. - README: add "What Changed in v1.3" and refresh Known Limitations (PTQ calibration shipped; scratch now sized to the widest layer). - threat_model.md: mark blinded LUT and output authentication as shipped, correct the confidence-threshold default (0/disabled, signed compare). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR is a head-to-toe audit and remediation pass on Minerva. It repairs the
documented host build, makes the public API match the docs, closes a Law I
verification gap, fixes correctness/memory-safety bugs across the BNN and MLP
paths, and adds the engine's first end-to-end test coverage. History is kept
as 9 small, atomic, independently-reviewable commits — happy to split into
themed PRs or squash if you prefer.
All work was verified first-hand: warning-free build,
ctestgreen, and everyengine architecture checked against an independent integer reference under
AddressSanitizer + UndefinedBehaviorSanitizer. Each bug fix ships with a test
that fails on the old code and passes on the new.
What's fixed (commit by commit)
Fix host CMake build — the documented
cmake && make && ctestflownever compiled: include paths omitted
src/security(somnv_engine.cfailed on
#include "mnv_blake2s.h") andMNV_SOURCESwas missingmnv_lut.c/mnv_outauth.c. Also fixes the one-Wall -Wextrawarningthat surfaced once the engine TUs compiled.
Repair public API + bind verified model to context —
mnv_run_with_model(used by the README quick-start and the example) was never declared in
minerva.h, so consumers got a compile error;mnv_run()was a stubreturning
MNV_ERR_CONFIG;mnv_verify_output()was declared but neverdefined. Now
mnv_init()binds the verified model into the context andmnv_run()/mnv_verify_output()operate on it. This also closes a Law Igap:
mnv_run_with_model()previously accepted any model pointer at runtime while only checking
ctx->verified(set for whatever modelmnv_initsaw), so
mnv_init(ctx, trusted)+mnv_run_with_model(ctx, other, …)ranunverified weights. Inference now rejects any model but the bound one.
Add end-to-end engine tests — the suite previously exercised only crypto
primitives;
mnv_initand the forward passes had zero coverage. New testsbuild a real ChaCha20+BLAKE2s model in memory and check output against an
independent reference, plus tamper/null/bad-ABI paths. Driven by
tests/host/run_engine_tests.sh(one build per arch, ASan+UBSan), wiredinto
ctest.Fix multi-layer BNN — (a)
mnv_config.hauto-definedMNV_QUANT_Q8whenever absent, silently overriding
MNV_QUANT_BINARY/Q4/Q15;(b) the BNN forward decrypted from offset 0 every layer (keystream advanced,
ciphertext pointer did not → garbage past layer 0); (c) it never consumed
the per-layer bias bytes the compiler emits. Now tracks the offset, decrypts
and applies biases, and binary quant is actually selectable.
Size per-layer buffers to the widest layer — the activation buffers,
weight scratch, and bias scratch all assumed layer 0 was widest; a wider
hidden/output layer overflowed them (ASan: stack-buffer-overflow). Sized to
the true max across layers, with a tightened static_assert.
Fix BNN dot product for sub-byte widths — when
in_sz % 8 != 0thebyte-popcount path mis-aligned per-neuron weights and counted padding bits
as agreements. Replaced with a branchless, bit-addressed XNOR dot
(constant-time, any alignment).
Fix confidence-check signedness — it compared
(uint8_t)max_val, so anegative max logit (e.g. -1 → 255) passed any threshold. Now a signed
comparison, with its own
MNV_ENABLE_CONFIDENCE_CHECKflag.Add LICENSE + .gitignore — the README and citation claim MIT and
reference a
LICENSEthat didn't exist;.gitignorekeeps device keys /generated models from being committed by accident. (See note below on the
copyright line.)
Docs + metadata sweep — version drift (header said 1.1.0, citation
1.2.0 → 1.3.0), a false "keystream precompute" claim in the
mnv_initdoc,the
mac"over plaintext" comment (it's over ciphertext / encrypt-then-MAC),blinded-LUT wording, reentrancy notes on the file-scope scratch buffers, a
v1.3 changelog, and threat-model corrections (blinded LUT + output auth
shipped; confidence default).
How to verify
Things that are your call (not asserted unilaterally)
but used a placeholder holder (
MINERVA project contributors). Tell me thename you want, or I'll drop the LICENSE from this PR and raise it as an issue
— entirely your decision.
ctx(vs. keeping the explicit-model entry points), and giving the BNNbiases. Both are isolated commits; I can rework or drop either.
Disclosure
This audit and the fixes were done with AI assistance (Claude); the
Co-Authored-Bytrailers reflect that. Every change was built, tested, andsanitizer-checked before submission.