Skip to content

rvf-wasm: verify_checksum always succeeds, witness_count always fails, store_open transposes count/dim and accepts anything #776

Description

@ruvnet

Three defects in the published @ruvector/rvf-wasm@0.1.9 microkernel, found while building an independent browser RVF viewer (rvQR) against the C-ABI. All reproduced directly against pkg/rvf_wasm_bg.wasm with a genuine 2304-byte container from rvf-cli (24 vectors, dim 16, 4 segments).

1. rvf_verify_checksum never detects corruption (most serious)

pristine container           -> 0   (success)
one payload byte flipped     -> 0   (success)
2304 bytes random noise      -> 0   (success)
empty buffer                 -> -1  (error)

It succeeds on everything except an empty buffer. A verification primitive that always returns success is worse than an absent one, because callers reasonably surface it as "integrity verified" — a false guarantee, and this is a security-relevant API.

Note the container also carries no reference CRC to compare against: I checked all 64 header bytes of every segment. So it isn't clear the function can do what its name promises with the current segment format. Either give segment headers a stored checksum and verify against it, or rename/remove the export so nobody builds a trust decision on it.

2. rvf_witness_count returns -1 for every non-zero length

rvf_witness_count(68) -> -1
rvf_witness_count(0)  ->  0

The demo container has a real 68-byte WITNESS segment. The chain cannot be counted or verified through the wasm API.

3. rvf_store_open transposes count/dim, and opens anything

store_open(valid container) -> handle 1, count=16, dim=24   (container is 24 vectors x dim 16)
store_open(random noise)    -> handle 2                      (no error)

The count/dim swap is confirmed by arithmetic: the Vec segment is 1734 bytes = 6 + 24 x (8 + 16 x 4), which only works for 24 vectors of dimension 16. Because the fields are swapped, rvf_store_query strides at the wrong record size and returns ids that are fragments of vector data rather than real ids.

Separately, store_open returns a valid-looking handle for arbitrary bytes — random noise and the wasm binary itself — so a successful open must not be treated as validation by any caller.

Reproduction

const w = (await WebAssembly.instantiate(fs.readFileSync('pkg/rvf_wasm_bg.wasm'), {})).instance.exports;
const put = b => { const p = w.rvf_alloc(b.length); new Uint8Array(w.memory.buffer, p, b.length).set(b); return p; };
const p = put(bytes);
w.rvf_verify_checksum(p, bytes.length);   // 0 for anything non-empty
const h = w.rvf_store_open(p, bytes.length);
w.rvf_store_count(h); w.rvf_store_dimension(h);  // transposed

(The kernel takes zero imports, so {} is the whole instantiation contract — pleasant to work with otherwise.)

Impact / workaround in the meantime

rvQR now surfaces all three as unavailable/warn rather than as passing checks, reads the Vec segment directly for count/dim/search instead of trusting store_open, and displays the kernel-vs-direct disagreement rather than silently picking a winner. Happy to send that probe harness if useful.

Also worth noting for the ADR-009 discussion in #775: the segment walker itself is correct — segment_count/segment_info enumerate all 4 segments accurately and account for every one of the 2304 bytes.

🤖 Generated with claude-flow

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions