Skip to content

[bug] approval resolution is race-prone and can double-resolve the same request #19

@nkanf-dev

Description

@nkanf-dev

Problem

ResolveApproval() can resolve the same approval more than once under concurrency.

The engine reads a pending approval under lock, mutates a local copy, unlocks, writes to SQLite, and only then writes the resolved state back into memory. During that window, a second resolver can still read the same approval as pending and also succeed.

The store layer makes this worse by using unconditional UPSERT semantics for both approval_states and attempt_grants, so the last writer wins instead of the second request failing closed.

Impact

  • Two operators can both receive a successful response while submitting conflicting decisions for the same approval
  • Final approval state depends on write ordering rather than first successful resolution
  • Attempt grants can be overwritten after an approval was already denied or approved differently
  • This breaks approval integrity in the most security-sensitive state transition in the system

Expected

Resolving an approval should be atomic and one-way:

  1. Exactly one resolver should be able to transition pending -> approved|denied|expired
  2. Concurrent resolvers should get a conflict once the first resolution commits
  3. Approval state and attempt grant persistence should be updated transactionally
  4. Tests should cover concurrent approve/deny races against the same approval id

Key code

  • internal/core/engine.goResolveApproval() unlocks before persistence and final in-memory update
  • internal/store/sqlite.goSaveApproval() uses unconditional UPSERT
  • internal/store/sqlite.goSaveAttemptGrant() also overwrites existing rows on conflict

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions