Thanks to visit codestin.com
Credit goes to github.com

Skip to content

bug: user_secrets and user_skills soft-delete guard triggers race with concurrent user soft-delete #28538

Description

@ThomasK33

Summary

The soft-delete guard triggers on user_secrets and user_skills check users.deleted without taking a lock on the parent users row, leaving open the race their own comments claim to close: an in-flight insert can read deleted = false, a concurrent soft-delete (UPDATE users SET deleted = true) plus its delete_deleted_user_resources() cleanup can commit, and the insert then commits afterwards, resurrecting a row for a soft-deleted user.

Affected triggers:

  • insert_user_secret_fail_if_user_deleted (coderd/database/migrations/000490_trigger_delete_user_secrets.up.sql): IF (SELECT deleted FROM users WHERE id = NEW.user_id LIMIT 1) THEN ... with no FOR ... lock clause.
  • insert_user_skill_fail_if_user_deleted (coderd/database/migrations/000502_user_skills.up.sql): PERFORM 1 FROM users WHERE id = NEW.user_id AND deleted = true LIMIT 1 with no lock.

Under READ COMMITTED both reads see a snapshot that a concurrent uncommitted soft-delete does not affect, and nothing orders the insert against the soft-delete transaction, so the guard is advisory rather than race-free.

Reference implementation

enforce_user_memories_insert_invariants() (added in coderd/database/migrations/000580_agent_memories.up.sql, #28423) closes the same window for user_memories:

  1. SELECT deleted FROM users WHERE id = NEW.user_id FOR NO KEY UPDATE; — the FOR NO KEY UPDATE parent-row lock serializes the insert against a concurrent soft-delete's UPDATE users (which takes NO KEY UPDATE/UPDATE row locks) without conflicting with the FOR KEY SHARE locks taken by FK validation on other child tables.
  2. After acquiring the lock, re-check deleted and raise check_violation when true. If the soft-delete committed first, its delete_deleted_user_resources() cleanup already ran, so no orphan row survives.

A deterministic regression test pattern (second connection + pg_stat_activity wait_event_type = 'Lock' synchronization, no sleeps) exists in coderd/database/agent_memories_test.go (TestUserMemories/SoftDeleteWinsConcurrentInsert).

Suggested fix

New migration replacing both trigger functions to take SELECT ... FROM users WHERE id = NEW.user_id FOR NO KEY UPDATE before evaluating deleted, mirroring enforce_user_memories_insert_invariants(), plus a one-time backfill/cleanup of any rows already orphaned by the race (the 000490 migration shows the backfill pattern for user_secrets).

Raised by coder-agents-review on #28423 (finding CRF-22).


Generated with mux • Model: anthropic:claude-fable-5 • Thinking: xhigh

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugexperimentalChanges that might not necessarily be merged, until its approved to proceed with.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions