Skip to content

Audit remediation: host build, public API + Law I model binding, BNN/MLP correctness, engine tests#1

Open
anjaustin wants to merge 9 commits into
kavishka-dot:mainfrom
anjaustin:audit-remediation
Open

Audit remediation: host build, public API + Law I model binding, BNN/MLP correctness, engine tests#1
anjaustin wants to merge 9 commits into
kavishka-dot:mainfrom
anjaustin:audit-remediation

Conversation

@anjaustin

Copy link
Copy Markdown

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, ctest green, and every
engine 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)

  1. Fix host CMake build — the documented cmake && make && ctest flow
    never compiled: include paths omitted src/security (so mnv_engine.c
    failed on #include "mnv_blake2s.h") and MNV_SOURCES was missing
    mnv_lut.c / mnv_outauth.c. Also fixes the one -Wall -Wextra warning
    that surfaced once the engine TUs compiled.

  2. Repair public API + bind verified model to contextmnv_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 stub
    returning MNV_ERR_CONFIG; mnv_verify_output() was declared but never
    defined. Now mnv_init() binds the verified model into the context and
    mnv_run()/mnv_verify_output() operate on it. This also closes a Law I
    gap
    : mnv_run_with_model() previously accepted any model pointer at run
    time while only checking ctx->verified (set for whatever model mnv_init
    saw), so mnv_init(ctx, trusted) + mnv_run_with_model(ctx, other, …) ran
    unverified weights. Inference now rejects any model but the bound one.

  3. Add end-to-end engine tests — the suite previously exercised only crypto
    primitives; mnv_init and the forward passes had zero coverage. New tests
    build 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), wired
    into ctest.

  4. Fix multi-layer BNN — (a) mnv_config.h auto-defined MNV_QUANT_Q8
    whenever 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.

  5. 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.

  6. Fix BNN dot product for sub-byte widths — when in_sz % 8 != 0 the
    byte-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).

  7. Fix confidence-check signedness — it compared (uint8_t)max_val, so a
    negative max logit (e.g. -1 → 255) passed any threshold. Now a signed
    comparison, with its own MNV_ENABLE_CONFIDENCE_CHECK flag.

  8. Add LICENSE + .gitignore — the README and citation claim MIT and
    reference a LICENSE that didn't exist; .gitignore keeps device keys /
    generated models from being committed by accident. (See note below on the
    copyright line.)

  9. 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_init doc,
    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

cmake -S . -B build -DMNV_TARGET=host && cmake --build build && ctest --test-dir build -V
bash tests/host/run_engine_tests.sh   # all arches, ASan+UBSan

Things that are your call (not asserted unilaterally)

  • LICENSE copyright line. I added the MIT text your README already promises,
    but used a placeholder holder (MINERVA project contributors). Tell me the
    name you want, or I'll drop the LICENSE from this PR and raise it as an issue
    — entirely your decision.
  • Two design choices you may want to weigh in on: caching the model in
    ctx (vs. keeping the explicit-model entry points), and giving the BNN
    biases. 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-By trailers reflect that. Every change was built, tested, and
sanitizer-checked before submission.

anjaustin and others added 9 commits June 2, 2026 18:18
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant