Skip to content

marmot-app: promoting a search-graph-only user into directory_users silently wipes their discovered follow edges (nondeterministic directory-search recall loss) #894

Description

@erskingardner

Severity: MEDIUM
Component: crates/marmot-app (src/directory/cache.rs, src/directory/methods.rs)

Summary

The bounded directory search graph (directory_search_graph_follows) is deliberately populated without promoting a contact-list author into directory_users, so follow edges stay available for bounded search while avoiding the unbounded social-graph crawl (mdk#687 / mdk#418). But every write into directory_users unconditionally re-snapshots the entry into the search graph with follows: Some(entry.follows.clone()). For a user promoted via the ordinary paths (profile / message-sender / relay-list / key-package), entry.follows is empty, so the snapshot deletes that account's previously-recorded search-graph follow edges and marks follows_known = true.

The result is nondeterministic, silent loss of exactly the follow data the search graph exists to preserve.

Location

  • crates/marmot-app/src/directory/cache.rs:183put_with_reason_locked unconditionally calls Self::put_search_graph_snapshot(conn, entry, now)? on every directory_users write.
  • crates/marmot-app/src/directory/cache.rs:262-279put_search_graph_snapshot always passes follows: Some(entry.follows.clone()).
  • crates/marmot-app/src/directory/cache.rs:338-398put_search_graph_record_locked: because record.follows is Some([]) (not None), it takes the replace_follow_rows(..., &[]) branch (DELETE all rows + insert nothing) and writes follows_known = 1.
  • crates/marmot-app/src/directory/cache.rs:80-89entry() reads directory_users only; a search-graph-only account returns None.
  • crates/marmot-app/src/directory/methods.rs:687-701directory_entry_for_account_id_with_handles reads only directory_users + shared storage, never the search graph.
  • crates/marmot-app/src/directory/methods.rs:822-837remember_directory_follow_edges_for_search (the search-graph-only writer whose edges get clobbered).
  • crates/marmot-app/src/directory/methods.rs:839-849remember_directory_profile builds entry via empty_directory_record (empty follows) for a not-yet-known user.

Concrete failure scenario (single relay batch, order-dependent)

  1. Alice's kind-3 contact list is ingested while Alice is not a known directory user → remember_directory_follow_edges_for_search(alice, [...]) records "alice → follows" in directory_search_graph_follows with follows_known = 1.
  2. In the same or a later batch, Alice's kind-0 profile (or a message from Alice) is processed → remember_directory_profile / remember_directory_message_sender builds an entry from empty_directory_record (Alice is not in directory_users, so directory_entry_for_account_id returns None) → save_directory_entryput_with_reason_lockedput_search_graph_snapshot with follows = Some([])replace_follow_rows("directory_search_graph_follows", &[]) deletes Alice's search-graph follow edges and leaves follows_known = 1 (now falsely asserting "Alice follows nobody").

Whether the edges survive depends purely on the relative processing order of kind-0 vs kind-3 within a batch: kind-0-after-kind-3 loses them; kind-0-before-kind-3 keeps them (the later remember_directory_follow_edges_for_search then also fills entry.follows). This is nondeterministic, silent recall degradation of directory search.

Impact

Silent, order-dependent loss of discovered follow edges, plus a false follows_known = true that makes search_graph_record return an empty follow set (rather than treating follows as unknown). This defeats the bounded-search design the search graph exists to provide.

Suggested fix

In put_search_graph_snapshot, pass follows: None when the promoted entry carries no follows (so put_search_graph_record_locked preserves the existing search-graph edges and existing follows_known state), or only snapshot follows when !entry.follows.is_empty(). More generally, a directory_users write should not be able to regress independently-sourced search-graph follow edges.

Dedup

Distinct from #884 (own-profile publish revert of kind:0 extra fields), #867 (directory extra-field ingest cap), and #744 (leaked inflight directory fetch entry). Closed #411 validates directory events before caching but does not touch this promotion-time clobber. No open/closed issue covers the search-graph follow-edge deletion on directory-user promotion.


Filed from a meticulous automated code-review pass; verified against source and deduplicated against the existing open/closed issue set.

Metadata

Metadata

Assignees

No one assigned

    Labels

    MEDIUMbugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions