36 lines
2.8 KiB
Markdown
36 lines
2.8 KiB
Markdown
# 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.
|