#36551: Race condition in savepoint ID generation causes duplicate identifiers
-------------------------------------+-------------------------------------
     Reporter:  Mijo Kristo          |                     Type:  Bug
       Status:  new                  |                Component:  Database
                                     |  layer (models, ORM)
      Version:                       |                 Severity:  Normal
     Keywords:  threading savepoint  |             Triage Stage:
  race condition thread safety       |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  1                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
 **Problem**

 The BaseDatabaseWrapper.savepoint() method contains a race condition where
 the operation self.savepoint_state += 1 is not atomic. This can lead to
 duplicate savepoint IDs in multithreaded environments.

 **Location**

 django/db/backends/base/base.py, line ~608
 **
 Problematic Code**

 thread_ident = _thread.get_ident()
 tid = str(thread_ident).replace("-", "")

 self.savepoint_state += 1  # Race condition here
 sid = "s%s_x%d" % (tid, self.savepoint_state)

 **Impact**

 - Multiple threads can read the same savepoint_state value
 - Results in duplicate savepoint IDs (e.g., "s123_x5", "s123_x5")
 - Causes database errors when rolling back to savepoints
 - Can lead to data corruption in high-concurrency applications
 - Affects applications using nested transaction.atomic() blocks

 **Reproduction**

 The race condition can be reproduced with this simple test:

 import threading, time

 counter = 0

 def buggy_increment():
     global counter
     temp = counter
     time.sleep(0.001)
     counter = temp + 1
     print(f'Thread got: {counter}')

 threads = [threading.Thread(target=buggy_increment) for _ in range(5)]
 for t in threads: t.start()
 for t in threads: t.join()

 print(f'Final: {counter} (should be 5)')

 Expected output: Final: 5
 Actual output: Final: 1 (lost updates due to race condition)

 **Real-world scenarios**

 This bug can manifest in production applications as:
 - "savepoint does not exist" database errors
 - Transaction rollback failures
 - Silent data corruption in e-commerce/banking applications
 - Inventory overselling in high-traffic scenarios

 **Proposed Fix
 **
 Wrap the increment operation in the existing _thread_sharing_lock:

 thread_ident = _thread.get_ident()
 tid = str(thread_ident).replace("-", "")

 with self._thread_sharing_lock:
     self.savepoint_state += 1
     sid = "s%s_x%d" % (tid, self.savepoint_state)

 This solution:
 - Uses Django's existing thread safety infrastructure
 - Has minimal performance impact
 - Maintains backward compatibility
 - Fixes the race condition completely

 **Testing**

 The fix can be verified by running the reproduction case above - with the
 fix, all threads will get unique values.

 Django's existing test suite should continue to pass as this change only
 affects the thread safety of savepoint generation without changing the
 API.

 **Additional Notes**

 - This is an ideal "easy pickings" issue for new contributors
 - The bug is timing-dependent and may not reproduce consistently
 - High-concurrency production environments are most affected
 - The fix leverages existing Django patterns for thread safety
-- 
Ticket URL: <https://code.djangoproject.com/ticket/36551>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-updates+unsubscr...@googlegroups.com.
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/01070198a37d0a61-e9102855-89d4-4e0d-90cc-4b43ae9ae2fb-000000%40eu-central-1.amazonses.com.

Reply via email to