fix(tags): order single-finding delete tag-count decrements so concurrent deletes cannot deadlock - #15664
Open
Maffooch wants to merge 1 commit into
Open
fix(tags): order single-finding delete tag-count decrements so concurrent deletes cannot deadlock#15664Maffooch wants to merge 1 commit into
Maffooch wants to merge 1 commit into
Conversation
A single-object finding delete (Finding.delete(), used by
DELETE /api/v2/findings/{id}/) removed its tags through tagulous's
per-object clear(), which issues one tag-count UPDATE per tag in the
manager's own order rather than ascending tag-id order. Because each
UPDATE takes a row lock, two concurrent single-finding deletes touching
an overlapping tag set could acquire those tag-row locks in opposite
orders and deadlock (Postgres 40P01, "while updating tuple ... in
relation dojo_tagulous_finding_tags").
Route the delete through bulk_remove_all_tags -- the same ascending
tag-id ordering already used by the bulk cascade delete (#15486) and the
import add path (#15652) -- so every tag-count mutation shares one lock
order and the cycle cannot form. Clearing the through rows first also
leaves the tagulous pre_delete handler nothing to decrement, so counts
are not double-processed. No schema change / no migration.
Adds FindingDeleteTagLockOrderTest asserting the decrements are issued in
ascending tag-id order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015i5bbWnQg2wKmQpvD7DRLz
|
This pull request contains a critical finding where the sensitive file 'dojo/finding/models.py' was modified by an unauthorized author, 'claude'.
🔴 Configured Sensitive Codepath Modified by Non-Allowed Author in
|
| Vulnerability | Configured Sensitive Codepath Modified by Non-Allowed Author |
|---|---|
| Description | File 'dojo/finding/models.py' matches configured sensitive codepath pattern 'dojo/finding/*.py' and was modified by 'claude' (commit 3a48e0c) who is not in the allowed authors list. |
We've notified @mtesauro.
Comment to provide feedback on these findings.
Report false positive: @dryrunsecurity fp [FINDING ID] [FEEDBACK]
Report low-impact: @dryrunsecurity nit [FINDING ID] [FEEDBACK]
Example: @dryrunsecurity fp drs_90eda195 This code is not user-facing
All finding details can be found in the DryRun Security Dashboard.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A single-object finding delete —
DELETE /api/v2/findings/{id}/→Finding.delete()→super().delete()— surfaced a Postgres deadlock to the client:Root cause.
dojo_tagulous_finding_tagsis the tagulous tag table (it holdscount), so the deadlocked statement is the per-tagcount = count - 1UPDATE, which takes a row lock on each tag. The synchronous single-object delete does not clear tags itself; it relies onsuper().delete(), whosepre_deletefires tagulous's per-objectclear(). Thatclear()issues one count UPDATE per tag in the manager's own iteration order, not in ascending tag-id order. Because the UPDATE order is the lock order, two concurrent single-finding deletes whose tag sets overlap can take the same tag-row locks in opposite orders, forming a cycle Postgres breaks by aborting one side (SQLSTATE40P01).This is the single-object twin of two deadlocks already fixed for the other tag-count paths:
bulk_remove_all_tags) decrement counts in a deterministic ascending tag-id order, and added anasync_delete_taskretry backstop.The synchronous
Finding.delete()path — used by the RESTdestroy()in both open source and the Pro override, which callssuper().destroy()→instance.delete()— was never routed through that shared ordering, so it can still deadlock against a concurrent single delete, bulk delete, or import.Fix.
Finding.delete()now removes the finding's tags viabulk_remove_all_tags(Finding, Finding.objects.filter(pk=self.pk))beforesuper().delete(). This issues the count decrements in the same ascending tag-id order every other tag-count path already uses, so all callers acquire the tag-row locks in one shared order and the cycle cannot form. Clearing the through rows first also leaves the tagulouspre_deletehandler nothing to decrement, so tag counts are not double-processed. Behavior is now consistent with the bulk cascade delete, which is the dominant deletion path.No migration. This is a pure ordering change to an existing code path — no model or schema change. A migration was neither generated nor needed; the fix reuses the already-shipped
bulk_remove_all_tagshelper.Downstream (Pro) impact. DefectDojo Pro's
FindingViewSet.destroy()overrides the OSS one but delegates tosuper().destroy(), which calls the model-levelFinding.delete()— so the fix covers the Pro delete path with no Pro-side change. Pro'sFindingpre_deletereceiver only dispatches integrator notifications and does not touch tag counts. Pro does not subclassFindingor overrideFinding.delete().Test results
Added
FindingDeleteTagLockOrderTestinunittests/test_tag_utils_bulk.py, mirroring the existingBulkRemoveAllTagsLockOrderTest(#15486) andBulkAddTagsToInstancesLockOrderTest(#15652). Three tags are attached to a finding in an order unrelated to their ids; the test captures the tag rows locked by the count UPDATEs duringfinding.delete()and asserts they are issued in ascending tag-id order. It fails on the old per-object-clear()order (name order,[2,3,1]) and passes once the delete is routed throughbulk_remove_all_tags([1,2,3]).The test is deterministic — it asserts the update/lock ordering, not a live race. As with #15652, a full Postgres-backed suite run could not be stood up in the ephemeral environment used to author this change; validation is being carried through CI.
ruff checkclean on both changed files (0.16.0, reporuff.toml).Documentation
No documentation change: no user-visible behavior change and no new setting.
Checklist
bugfixbranch.Generated by Claude Code