Skip to content

SBOM Deletion Concurrency Race Condition Analysis #2191

@bxf12315

Description

@bxf12315

SBOM Deletion Concurrency Race Condition Analysis

Overview

This document analyzes a potential race condition that can occur when deleting SBOMs concurrently in a multi-threaded environment. The issue involves orphaned PURL (Package URL) records being left behind when two or more SBOMs referencing the same qualified PURL are deleted simultaneously.

Problem Description

Scenario

Consider the following scenario:

  • SBOM1 and SBOM2 both reference the same qualified_purl A
  • Two concurrent transactions attempt to delete both SBOMs
  • Each transaction executes in a separate thread/connection

Expected Behavior

When both SBOMs are deleted, the qualified_purl A should be cleaned up since it's no longer referenced by any SBOM.

Actual Behavior

Due to a race condition, qualified_purl A may become orphaned data (garbage) even though all its references have been removed.

Timeline Analysis

The following timeline demonstrates how the race condition occurs:

Time | Transaction 1 (Delete SBOM1)           | Transaction 2 (Delete SBOM2)
-----|------------------------------------------|------------------------------------------
T1   | Capture qualified_purl_ids = [A]        |
T2   |                                          | Capture qualified_purl_ids = [A]
T3   | Delete SBOM1 (CASCADE removes refs)      |
T4   |                                          | Delete SBOM2 (CASCADE removes refs)
T5   | Execute GC query - sees SBOM2's ref      |
     | (not yet committed, still visible)       |
T6   |                                          | Execute GC query - sees SBOM1's ref
     |                                          | (not yet committed, still visible)
T7   | Commit                                   |
T8   |                                          | Commit

Detailed Analysis

Step 1: Capture Qualified PURL IDs

Both transactions capture the qualified PURL IDs before deletion:

// Line 112-119 in sbom.rs
let qualified_purl_ids: Vec<Uuid> = sbom_package_purl_ref::Entity::find()
    .select_only()
    .column(sbom_package_purl_ref::Column::QualifiedPurlId)
    .filter(sbom_package_purl_ref::Column::SbomId.eq(id))
    .into_tuple()
    .all(connection)
    .await?;
  • T1: Transaction 1 captures qualified_purl_ids = [A]
  • T2: Transaction 2 captures qualified_purl_ids = [A]

Step 2: Delete SBOM with CASCADE

Both transactions delete their respective SBOMs:

-- Line 126-128 in sbom.rs
DELETE FROM sbom WHERE sbom_id=$1 RETURNING source_document_id

The CASCADE constraint automatically deletes related records:

  • sbom_package entries

  • sbom_package_purl_ref entries (references to qualified PURLs)

  • T3: Transaction 1 deletes SBOM1, removing its reference to PURL A

  • T4: Transaction 2 deletes SBOM2, removing its reference to PURL A

Step 3: Execute Garbage Collection Query

Both transactions execute the GC query to clean up orphaned PURLs:

-- From gc_purls_after_sbom_deletion.sql
WHERE NOT EXISTS (
    -- Check if still referenced by other SBOMs
    SELECT 1 FROM sbom_package_purl_ref sppr
    WHERE sppr.qualified_purl_id = sq.id
)

At T5 (Transaction 1's GC query):

  • SBOM1's reference to A has been deleted (T3)
  • But SBOM2's deletion is not yet committed (T4)
  • Under READ COMMITTED isolation, Transaction 1 still sees SBOM2's reference to A
  • Conclusion: A still has references → Do not delete A

At T6 (Transaction 2's GC query):

  • SBOM2's reference to A has been deleted (T4)
  • But SBOM1's deletion is not yet committed (T7)
  • Under READ COMMITTED isolation, Transaction 2 still sees SBOM1's reference to A
  • Conclusion: A still has references → Do not delete A

Step 4: Commit Transactions

  • T7: Transaction 1 commits
  • T8: Transaction 2 commits

Final Result

After both transactions commit:

  • All references to qualified_purl A in sbom_package_purl_ref table are deleted
  • qualified_purl A itself still exists
  • A becomes orphaned data (garbage)

Probability of Occurrence

The likelihood of this race condition occurring depends on:

  1. Frequency of Concurrent Deletions

    • Higher if the system frequently processes multiple SBOM deletion requests simultaneously
    • More likely in high-traffic environments
  2. Number of Shared Qualified PURLs

    • Higher if multiple SBOMs frequently reference the same packages
    • Common in software supply chains where the same dependencies appear across multiple SBOMs
  3. Timing of Requests

    • Requires two deletion requests to arrive within a narrow time window
    • More likely with high concurrency and slow database operations

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions