2.8 KiB
Audit: Signal Revocation Controls
Security
User isolation: revoke_signal(user_id, scope) stores revocations keyed by user_id. The executor only applies revocations when query.for_user matches the stored user_id. Cross-user revocation is structurally impossible — the index is keyed by user and the executor check is per-user. Test R6 verifies this.
No privilege escalation: Revocations reduce signal contributions — they can only lower scores, never raise them. A malicious caller cannot use revocations to boost content.
Storage key: Tag::Revocation = 0x0F is isolated in the users_engine partition. It cannot collide with item, creator, or community data.
No WAL exposure: Revocations are not WAL-backed. They are persisted directly to fjall (users_engine). This means revocations are not replicated via WAL shipping — acceptable for M10 scope; the spec does not require multi-node consistency for revocations.
Correctness Under Concurrency
RevocationIndex uses DashMap<u64, Vec<SignalRevocation>>. Reads via is_suppressed take a shared DashMap guard (concurrent reads do not block each other). Writes via upsert and cancel take exclusive entry guards (per-shard locks). The executor holds an Arc<RevocationIndex> and calls is_suppressed on the hot path — no locks are held across scoring iterations.
Consistency: persist_revocation writes to storage before upsert to memory on the create path (durable before visible). On the cancel path, memory is updated first (idempotent) then re-persisted. There is a brief window between memory cancel and storage cancel where a crash would leave the in-memory index as cancelled but storage as active. On restart, rebuild_revocations would load the active record. This is a known limitation documented in comments — acceptable for M10; revocation is a user-preferences feature, not a safety gate.
Performance
is_signal_revoked_for_entityis called per-signal per-entity during scoring. It performs: oneresolve_signal_type(schema lookup, O(1)), oneentry_last_update_ns(DashMap get, O(1)), oneRevocationIndex::is_suppressed(DashMap get + O(R) Vec scan where R = active revocations, typically 0).- The fast path (no revocations for user) exits after a single DashMap miss. This is effectively free.
- For users with revocations: each signal costs one extra DashMap get + O(R) linear scan. At R=5 and 27 sort variants + N boosts, this is well within latency budget.
API Completeness
All three public API methods are present: revoke_signal, cancel_revocation, list_revocations. revocation_index() accessor is provided for executor wiring. rebuild_revocations is called on startup open path. All are exported from lib.rs.
Findings
No security, correctness, or performance issues requiring remediation.
Verdict
Approved. No audit findings.