git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
20 KiB
Security Audit Report: Cognitum Gate Implementation
Audit Date: 2026-01-17 Auditor: Claude Code Security Review Agent Scope: cognitum-gate-kernel, cognitum-gate-tilezero, mcp-gate Risk Classification: Uses CVSS-style severity (Critical/High/Medium/Low)
Executive Summary
This security audit identified 17 security issues across the cognitum-gate implementation:
| Severity | Count | Categories |
|---|---|---|
| Critical | 2 | Cryptographic bypass, signature truncation |
| High | 4 | Memory safety, unsafe code, race conditions |
| Medium | 6 | Input validation, integer overflow, DoS vectors |
| Low | 5 | Information disclosure, edge cases |
Recommendation: The Critical issues in permit.rs must be fixed before production deployment as they completely bypass signature verification.
Critical Issues
CGK-001: Signature Verification Bypass (CRITICAL)
Severity: Critical
Location: /home/user/ruvector/crates/cognitum-gate-tilezero/src/permit.rs:136-153
CVSS: 9.8 (Critical)
Description:
The Verifier::verify() function does not actually verify signatures. It computes a hash from the token content and compares it to... the same hash computed from the same content. This comparison always succeeds.
// Lines 147-151 - BROKEN VERIFICATION
let expected_hash = blake3::hash(&content);
if hash.as_bytes() != expected_hash.as_bytes() {
return Err(VerifyError::HashMismatch);
}
// hash == expected_hash ALWAYS - computed from same content!
Impact: Any attacker can forge permit tokens. The cryptographic authentication is completely bypassed. All gate decisions can be spoofed.
Recommended Fix:
pub fn verify(&self, token: &PermitToken) -> Result<(), VerifyError> {
let content = token.signable_content();
let hash = blake3::hash(&content);
// Reconstruct full 64-byte signature
// REQUIRES: Store full signature in token, not truncated 32 bytes
let signature = ed25519_dalek::Signature::from_bytes(&token.signature)
.map_err(|_| VerifyError::SignatureFailed)?;
// Actually verify the signature
self.verifying_key
.verify(hash.as_bytes(), &signature)
.map_err(|_| VerifyError::SignatureFailed)?;
Ok(())
}
CGK-002: Ed25519 Signature Truncation (CRITICAL)
Severity: Critical
Location: /home/user/ruvector/crates/cognitum-gate-tilezero/src/permit.rs:103-111
CVSS: 9.1 (Critical)
Description:
The sign_token function truncates the 64-byte Ed25519 signature to 32 bytes:
// Line 109 - Discards half the signature!
token.mac.copy_from_slice(&signature.to_bytes()[..32]);
Ed25519 signatures are 64 bytes. Truncating to 32 bytes makes reconstruction impossible and verification meaningless.
Impact: Combined with CGK-001, this makes signature verification completely non-functional. Even if verification was fixed, the stored signature cannot be reconstructed.
Recommended Fix:
// In PermitToken struct - change mac field:
pub signature: [u8; 64], // Full Ed25519 signature
// In sign_token:
token.signature.copy_from_slice(&signature.to_bytes());
High Severity Issues
CGK-003: Unsafe Global Mutable State Without Synchronization
Severity: High
Location: /home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:413
CVSS: 7.5
Description:
The global TILE_STATE is accessed through static mut without any synchronization primitives:
static mut TILE_STATE: Option<TileState> = None;
All WASM export functions (init_tile, ingest_delta, tick, etc.) access this mutable static unsafely.
Impact: In multi-threaded contexts or if WASM threading is enabled, this creates data races leading to undefined behavior, memory corruption, or security bypasses.
Recommended Fix:
use core::cell::UnsafeCell;
use core::sync::atomic::{AtomicBool, Ordering};
struct TileStateHolder {
initialized: AtomicBool,
state: UnsafeCell<Option<TileState>>,
}
// Or for single-threaded WASM, use OnceCell pattern
static TILE_STATE: once_cell::sync::OnceCell<RefCell<TileState>> = OnceCell::new();
CGK-004: Unsafe Raw Pointer Dereference Without Validation
Severity: High
Location: /home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:207-210
CVSS: 7.3
Description:
The ingest_delta_raw function casts a raw pointer without checking alignment:
pub unsafe fn ingest_delta_raw(&mut self, ptr: *const u8) -> bool {
let delta = unsafe { &*(ptr as *const Delta) }; // No alignment check!
self.ingest_delta(delta)
}
Delta likely requires alignment > 1 byte. Misaligned access is undefined behavior.
Impact: Misaligned memory access causes undefined behavior on some architectures, potentially leading to crashes or exploitable memory corruption.
Recommended Fix:
pub unsafe fn ingest_delta_raw(&mut self, ptr: *const u8) -> bool {
// Check alignment
if (ptr as usize) % core::mem::align_of::<Delta>() != 0 {
return false;
}
// Check null
if ptr.is_null() {
return false;
}
let delta = unsafe { &*(ptr as *const Delta) };
self.ingest_delta(delta)
}
CGK-005: Bump Allocator Race Condition
Severity: High
Location: /home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:70-99
CVSS: 7.0
Description: The bump allocator uses static mutable variables without synchronization:
static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
static mut HEAP_PTR: usize = 0;
unsafe impl GlobalAlloc for BumpAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe {
let aligned = (HEAP_PTR + align - 1) & !(align - 1); // Race condition!
// ...
HEAP_PTR = aligned + size; // Non-atomic update!
}
}
}
Impact: Concurrent allocations could return overlapping memory regions, leading to memory corruption.
Recommended Fix:
use core::sync::atomic::{AtomicUsize, Ordering};
static HEAP_PTR: AtomicUsize = AtomicUsize::new(0);
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
loop {
let current = HEAP_PTR.load(Ordering::Acquire);
let aligned = (current + layout.align() - 1) & !(layout.align() - 1);
let new_ptr = aligned + layout.size();
if new_ptr > HEAP_SIZE {
return core::ptr::null_mut();
}
if HEAP_PTR.compare_exchange_weak(current, new_ptr, Ordering::Release, Ordering::Relaxed).is_ok() {
return unsafe { HEAP.as_mut_ptr().add(aligned) };
}
}
}
CGK-006: Unchecked Union Access in Delta Processing
Severity: High
Location: /home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:288-321
CVSS: 6.8
Description:
The apply_delta function uses unsafe union access based on a tag field:
DeltaTag::EdgeAdd => {
let ea = unsafe { delta.get_edge_add() }; // Trusts tag
// ...
}
If the tag is corrupted or maliciously set, accessing the wrong union variant leads to undefined behavior.
Impact: A malformed delta with mismatched tag/data could cause memory corruption or information disclosure.
Recommended Fix:
- Add validation of delta integrity (checksum/hash)
- Use a safe enum representation instead of tagged union where possible
- Add bounds checking on union field values after extraction
Medium Severity Issues
CGK-007: Division by Zero in Threshold Computation
Severity: Medium
Location: /home/user/ruvector/crates/cognitum-gate-tilezero/src/decision.rs:223-228
CVSS: 5.9
Description: Pre-computed reciprocals can cause division by zero:
let inv_min_cut = 1.0 / thresholds.min_cut; // Zero if min_cut == 0
let inv_max_shift = 1.0 / thresholds.max_shift; // Zero if max_shift == 0
let inv_tau_range = 1.0 / (thresholds.tau_permit - thresholds.tau_deny); // Zero if equal
Impact: Results in infinity/NaN values that propagate through decision logic, potentially causing incorrect permit/deny decisions.
Recommended Fix:
pub fn new(thresholds: GateThresholds) -> Result<Self, ThresholdError> {
if thresholds.min_cut == 0.0 || thresholds.max_shift == 0.0 {
return Err(ThresholdError::ZeroThreshold);
}
if (thresholds.tau_permit - thresholds.tau_deny).abs() < f64::EPSILON {
return Err(ThresholdError::EqualTauRange);
}
// ... continue with safe reciprocal computation
}
CGK-008: Integer Overflow in Token TTL Check
Severity: Medium
Location: /home/user/ruvector/crates/cognitum-gate-tilezero/src/permit.rs:31-33
CVSS: 5.3
Description: The validity check can overflow:
pub fn is_valid_time(&self, now_ns: u64) -> bool {
now_ns <= self.timestamp + self.ttl_ns // Overflow possible!
}
If timestamp + ttl_ns overflows u64, the comparison becomes incorrect.
Impact: Tokens with very large timestamps or TTLs could have incorrect validity checks, either expiring immediately or never expiring.
Recommended Fix:
pub fn is_valid_time(&self, now_ns: u64) -> bool {
self.timestamp.checked_add(self.ttl_ns)
.map(|expiry| now_ns <= expiry)
.unwrap_or(true) // If overflow, consider perpetually valid or use saturating
}
CGK-009: Unbounded History Growth / DoS Vector
Severity: Medium
Location: /home/user/ruvector/crates/cognitum-gate-tilezero/src/receipt.rs:124-132, 169-185
CVSS: 5.0
Description:
The ReceiptLog uses a HashMap that grows unboundedly:
pub struct ReceiptLog {
receipts: HashMap<u64, WitnessReceipt>, // Grows forever
// ...
}
Additionally, verify_chain_to iterates from 0 to sequence number, making it O(n) in chain length.
Impact: Memory exhaustion attack by generating many decisions. Chain verification becomes increasingly slow.
Recommended Fix:
const MAX_RECEIPTS: usize = 100_000;
pub fn append(&mut self, receipt: WitnessReceipt) -> Result<(), LogFullError> {
if self.receipts.len() >= MAX_RECEIPTS {
// Implement pruning or return error
self.prune_old_receipts();
}
// ...
}
// Use rolling window verification instead of full chain
pub fn verify_recent(&self, window: usize) -> Result<(), ChainVerifyError> {
let start = self.latest_sequence.saturating_sub(window as u64);
// Verify only recent entries
}
CGK-010: Unchecked Array Index in Evidence Processing
Severity: Medium
Location: /home/user/ruvector/crates/cognitum-gate-kernel/src/evidence.rs:407-416
CVSS: 4.8
Description: Window access uses unchecked indexing:
let idx = self.window_head as usize;
// Line 410: Assumes idx < WINDOW_SIZE
unsafe {
*self.window.get_unchecked_mut(idx) = ObsRecord { obs, tick };
}
The bit masking on line 413 is correct, but it happens AFTER the unsafe access.
Impact:
If window_head is corrupted, out-of-bounds write occurs.
Recommended Fix:
// Apply mask BEFORE access
let idx = (self.window_head as usize) & (WINDOW_SIZE - 1);
self.window[idx] = ObsRecord { obs, tick }; // Safe bounds-checked access
self.window_head = (self.window_head + 1) as u16;
CGK-011: Panic on System Time Before Epoch
Severity: Medium
Location: /home/user/ruvector/crates/cognitum-gate-tilezero/src/lib.rs:173-176
CVSS: 4.5
Description: The time computation can panic:
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap() // Panics if system time < epoch!
.as_nanos() as u64;
Impact: If system time is misconfigured (before 1970), the gate panics and becomes unavailable.
Recommended Fix:
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(std::time::Duration::ZERO)
.as_nanos() as u64;
CGK-012: Processing Rate Division by Zero
Severity: Medium
Location: /home/user/ruvector/crates/cognitum-gate-kernel/src/report.rs:284-289
CVSS: 4.0
Description:
pub fn processing_rate(&self) -> f32 {
if self.tick_time_us == 0 {
0.0 // Handled correctly
} else {
(self.deltas_processed as f32) / (self.tick_time_us as f32)
}
}
This is actually handled correctly. However, the check should use floating point division behavior documentation.
Status: No action required - correctly implemented.
Low Severity Issues
CGK-013: Tick Time Truncation
Severity: Low
Location: /home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:257
CVSS: 3.5
Description: Tick time is cast from u32 to u16:
report.tick_time_us = (tick_end - tick_start) as u16; // Truncates if > 65535
Impact: Ticks longer than ~65ms will have incorrect timing metrics, affecting performance analysis.
Recommended Fix:
report.tick_time_us = (tick_end - tick_start).min(u16::MAX as u32) as u16;
CGK-014: Silent JSON Serialization Failure
Severity: Low
Location: /home/user/ruvector/crates/cognitum-gate-tilezero/src/receipt.rs:82-83
CVSS: 3.1
Description:
pub fn hash(&self) -> [u8; 32] {
let json = serde_json::to_vec(self).unwrap_or_default(); // Silent failure!
*blake3::hash(&json).as_bytes()
}
Impact: If serialization fails, an empty hash is computed, potentially causing hash collisions.
Recommended Fix:
pub fn hash(&self) -> Result<[u8; 32], HashError> {
let json = serde_json::to_vec(self)?;
Ok(*blake3::hash(&json).as_bytes())
}
CGK-015: Information Disclosure in Error Messages
Severity: Low
Location: /home/user/ruvector/crates/mcp-gate/src/tools.rs:292-355
CVSS: 3.0
Description: Error messages expose internal state details:
format!("Min-cut {:.3} below threshold {:.3}", mincut_value, self.thresholds.min_cut)
format!("E-value {:.4} indicates strong evidence of incoherence", summary.evidential.e_value)
Impact: Exposes exact threshold values and internal metrics to clients, aiding targeted attacks.
Recommended Fix: Return generic error codes to external clients; log detailed messages internally only.
CGK-016: No Input Size Limits on Tool Calls
Severity: Low
Location: /home/user/ruvector/crates/mcp-gate/src/tools.rs:126-159
CVSS: 2.8
Description:
The call_tool function deserializes JSON without size limits:
let request: PermitActionRequest = serde_json::from_value(call.arguments)
.map_err(|e| McpError::InvalidRequest(e.to_string()))?;
Impact: Very large JSON payloads could cause memory exhaustion.
Recommended Fix:
Add a size limit check before deserialization or use serde_json with size limits.
CGK-017: Hardcoded Escalation Timeout
Severity: Low
Location: /home/user/ruvector/crates/mcp-gate/src/tools.rs:194
CVSS: 2.5
Description:
timeout_ns: 300_000_000_000, // 5 minutes - hardcoded
Impact: Cannot adjust escalation timeout without code changes; not a direct security issue but affects operational security.
Recommended Fix:
Make configurable via GateThresholds or environment variable.
Recommendations Summary
Immediate Actions (Critical/High)
- Fix signature verification (CGK-001, CGK-002) - This is a complete authentication bypass
- Add synchronization to global state (CGK-003, CGK-005) - Prevents data races
- Add alignment/null checks to raw pointer operations (CGK-004)
- Add validation to delta processing (CGK-006)
Short-term Actions (Medium)
- Validate thresholds before computing reciprocals (CGK-007)
- Use checked arithmetic for token TTL (CGK-008)
- Bound receipt log size and optimize chain verification (CGK-009)
- Reorder bit masking in evidence window (CGK-010)
- Handle system time edge cases (CGK-011)
Long-term Actions (Low)
- Sanitize error messages for external clients (CGK-015)
- Add input size limits (CGK-016)
- Make operational parameters configurable (CGK-017)
Unsafe Code Audit Summary
| File | Unsafe Blocks | Safety Concerns |
|---|---|---|
| kernel/lib.rs | 8 | Global state, raw pointers, union access |
| kernel/shard.rs | 14 | Unchecked array indexing (performance-critical) |
| kernel/evidence.rs | 4 | Unchecked window access |
| kernel/report.rs | 0 | None |
| tilezero/lib.rs | 0 | None |
| tilezero/permit.rs | 0 | None (but cryptographic issues) |
| tilezero/receipt.rs | 0 | None |
| tilezero/decision.rs | 0 | None |
| mcp-gate/tools.rs | 0 | None |
The kernel crate uses unsafe code extensively for performance optimization. Each instance should be audited against its safety invariants.
Testing Recommendations
- Fuzzing: Apply
cargo-fuzzto delta parsing and token decoding - Property testing: Use
proptestfor invariant validation - Miri: Run
cargo miri testto detect undefined behavior - Memory sanitizers: Test with AddressSanitizer and MemorySanitizer
Compliance Notes
- No timing attacks identified in the cryptographic code (uses constant-time libraries)
- Key generation uses
OsRngwhich is cryptographically secure - Hash function (blake3) is modern and appropriate
- Signature scheme (Ed25519) is appropriate but implementation is broken
Appendix A: Delta Module Analysis
File: /home/user/ruvector/crates/cognitum-gate-kernel/src/delta.rs
The delta module implements a tagged union (DeltaPayload) for graph updates. The design is sound but has some security considerations:
Union Safety
The DeltaPayload union is correctly sized (8 bytes for all variants) with compile-time assertions. The unsafe accessor methods (get_edge_add, get_edge_remove, etc.) correctly require the caller to verify the tag before access.
Current Implementation (Lines 379-401):
/// Get the edge add payload (unsafe: caller must verify tag)
pub unsafe fn get_edge_add(&self) -> &EdgeAdd {
unsafe { &self.payload.edge_add }
}
Recommendation: Consider adding debug assertions:
#[inline]
pub unsafe fn get_edge_add(&self) -> &EdgeAdd {
debug_assert_eq!(self.tag, DeltaTag::EdgeAdd, "Invalid tag for EdgeAdd access");
unsafe { &self.payload.edge_add }
}
Alignment Considerations
The Delta struct is aligned to 16 bytes (#[repr(C, align(16))]), which is correct for WASM and most architectures. However, when deserializing from raw bytes (as in ingest_delta_raw), alignment must be verified.
Appendix B: Threat Model Summary
| Threat | Likelihood | Impact | Mitigation Status |
|---|---|---|---|
| Token forgery (CGK-001/002) | High | Critical | NOT MITIGATED |
| Memory corruption via malformed delta | Medium | High | Partial (tag check, no integrity check) |
| DoS via memory exhaustion | Medium | Medium | Partial (fixed buffers, but unbounded log) |
| Race condition exploitation | Low | High | NOT MITIGATED (single-threaded WASM assumed) |
| Timing side-channel | Low | Low | Mitigated (constant-time crypto libs) |
Appendix C: Verification Status of Unsafe Code Invariants
| Location | Invariant | Verified By |
|---|---|---|
| shard.rs:450 | source < MAX_SHARD_VERTICES | Bounds check at line 445 |
| shard.rs:457 | degree <= MAX_DEGREE | Struct invariant (add_edge checks) |
| shard.rs:576-577 | root < MAX_SHARD_VERTICES | Loop construction |
| evidence.rs:410 | idx < WINDOW_SIZE | BROKEN - mask applied after access |
| lib.rs:208 | ptr aligned to Delta alignment | NOT VERIFIED |
| lib.rs:292 | tag matches payload variant | Tag set during construction only |
Report generated by Claude Code Security Review Agent Classification: Internal Security Document