tidaldb/.sdlc/features/m10-signal-revocation-controls/audit.md

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_entity is called per-signal per-entity during scoring. It performs: one resolve_signal_type (schema lookup, O(1)), one entry_last_update_ns (DashMap get, O(1)), one RevocationIndex::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.