Skip to content

fix(reporting): serialize the duplicate check so concurrent agents cannot file a finding twice - #1469

Open
samirhvbr wants to merge 2 commits into
usestrix:mainfrom
samirhvbr:fix/dedupe-concurrent-filing
Open

samirhvbr wants to merge 2 commits into
usestrix:mainfrom
samirhvbr:fix/dedupe-concurrent-filing

Conversation

@samirhvbr

@samirhvbr samirhvbr commented Oct 6, 2026 •

Copy link
Copy Markdown

Fixes #627.

Problem

The duplicate check in create_vulnerability_report awaits a model call and compares the candidate with the findings that were stored when it started. Two agents filing the same finding at the same moment each see a list without the other's report, both pass the check, and the run stores the finding twice.

Reproduced against the real _do_create with the real check_duplicate, with one unrelated finding already stored and a stand-in model that pauses like a real call and judges against the reports in the request it is handed:

  • two concurrent identical filings: both stored
  • a sequential second filing of the same finding: correctly rejected as a duplicate

So the check works; it just is not atomic with the add.

Fix

Hold a per-run asyncio.Lock (ReportState.dedupe_lock) across read stored findings → duplicate check → add in create_vulnerability_report. Distinct findings still go through, one duplicate check at a time. The candidate and field dicts are built before the lock because they do not depend on the stored list, so the diff is mostly indentation: git diff -w is 13 lines.

create_dependency_report is left alone on purpose. For a dependency candidate check_duplicate decides from the CVE / ecosystem / package identity without calling the model, so it never awaits and cannot interleave with another filing. (An earlier revision of this PR also locked that path; it could not be shown to race, so it was dropped.)

I looked at #633 first. Its lock covers only the read and its re-check is an exact-title match, and its test re-implements the sequence instead of calling the real function, so I did not build on it.

Trade-off

Filings are serialized. Each waits for the dedupe call (a few seconds) while another is in flight; that is the cost of the check being correct, and runs file a handful of findings.

Tests

tests/test_reporting_dedupe_race.py drives the real _do_create and the real check_duplicate. The real check returns at once while nothing is stored, so each test stores one unrelated finding first and only the model is replaced:

  • the same finding filed concurrently is stored once, and both filings went through the model
  • distinct findings filed together are all stored
  • a failing dedupe call does not leave the lock held (a later filing completes)

Without the lock the same-finding test fails ([True, True]: both filings succeed); with it all three pass. The settings the test needs are isolated from the developer's stored config. ruff format, ruff check and mypy are clean on the touched files, and the neighbouring reporting, dedupe and state tests pass (235).

🤖 Generated with Claude Code

https://claude.ai/code/session_01TavwVMH7PvrD68JTDfd6Hb

…nnot file a finding twice

The duplicate check awaits an LLM call and compares the candidate with the
findings that were stored when it started. Two agents filing the same finding
at the same moment each saw a list without the other's report, passed the
check and both stored it. Reproduced against the real create functions with a
slow check: three concurrent identical filings were stored three times, while
a sequential fourth was correctly rejected.

Hold a per-run asyncio lock across read existing -> check -> add in both
create_vulnerability_report and create_dependency_report. Distinct findings
still go through, one duplicate check at a time. The candidate and field dicts
are built before the lock (they do not depend on the stored list), so the
diff is mostly indentation: `git diff -w` is 13 lines.

Fixes usestrix#627

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TavwVMH7PvrD68JTDfd6Hb
@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Medium risk] Adds concurrency control to the duplicate-finding check.

The code appears safe to merge, but the race tests should cover a path where the real duplicate check pauses.

Findings

  1. P2 Race tests miss real waits ▶
Fix with agent prompt
### Issue 1
tests/test_reporting_dedupe_race.py:45-49
Each test starts with no stored findings, but `_slow_check` always pauses. The real `check_duplicate` returns right away for an empty list and for matching dependency CVE/package pairs. These tests could pass even if the race returns on a path that waits for the model. Seed an unrelated finding in the vulnerability test and test a dependency case that reaches the model check.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Both report creation paths now check saved findings and add new ones one at a time, so concurrent matching filings don’t both get stored.

  • Concurrent matching findings get saved only once.

Reviews (1) · Last reviewed commit: "fix(reporting): serialize the duplicate ..."

Comment thread tests/test_reporting_dedupe_race.py Outdated
Comment on lines +45 to +49
async def _slow_check(candidate: dict[str, Any], existing: list[dict[str, Any]]) -> dict[str, Any]:
"""Stand in for the real check: it judges the candidate against the snapshot it is handed."""
await asyncio.sleep(0.05) # the real one is a model call
for report in existing:
if report.get("title") == candidate["title"]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Race tests miss real waits

Each test starts with no stored findings, but _slow_check always pauses. The real check_duplicate returns right away for an empty list and for matching dependency CVE/package pairs. These tests could pass even if the race returns on a path that waits for the model. Seed an unrelated finding in the vulnerability test and test a dependency case that reaches the model check.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/test_reporting_dedupe_race.py
Line: 45-49

Comment:
**Race tests miss real waits**

Each test starts with no stored findings, but `_slow_check` always pauses. The real `check_duplicate` returns right away for an empty list and for matching dependency CVE/package pairs. These tests could pass even if the race returns on a path that waits for the model. Seed an unrelated finding in the vulnerability test and test a dependency case that reaches the model check.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

samirhvbr pushed a commit to samirhvbr/strix that referenced this pull request Oct 6, 2026
Bump to 1.7.0+shvia.22 and add the FORK.md row plus the four contributions
(usestrix#1469 dedupe lock, usestrix#1470 whitebox repository targets, usestrix#1471 test isolation from
the real home, usestrix#1472 trust fields in the PDF report). CLAUDE.md now says the suite
no longer writes the real ~/.strix, and that `import litellm` loads the current
directory's .env unless LITELLM_MODE=PRODUCTION.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TavwVMH7PvrD68JTDfd6Hb
Resolves the conflicts with the retry and reporting changes that landed on main since
this branch was opened. The resulting tree is the PR rebased onto main, tested there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TavwVMH7PvrD68JTDfd6Hb

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]Concurrent agents can bypass deduplication and file the same vulnerability twice

1 participant