Skip to content

Commit 6409da4

Browse files
authored
fix(locking): Replace object_id-based locks with thread-local storage (#226)
The previous implementation stored locks in a class variable indexed by Thread.current.object_id, which had three critical defects: 1. Object ID reuse: Ruby can reuse object IDs after garbage collection, allowing new threads to inadvertently access stale locks from dead threads 2. Race conditions: The shared @@locks class variable was not thread-safe, creating potential race conditions 3. Memory leaks: Dead thread entries were not automatically cleaned up This fix replaces the class variable with Ruby's built-in thread-local storage (Thread.current[:double_entry_locks]), which: - Ties data directly to the thread object, not its object ID - Provides isolated storage per thread - Automatically cleans up when threads terminate
1 parent f4bb6cf commit 6409da4

2 files changed

Lines changed: 10 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Fix critical thread-safety issues in locking mechanism by replacing object_id-based lock storage with proper thread-local storage. This resolves object ID reuse vulnerabilities, race conditions, and memory leaks ([#226]).
13+
14+
### Changed
15+
1016
- Run the test suite against Rails 8.1, 8.0, 7.2, and Ruby 4.0, 3.4, 3.3, 3.2 ([#225]).
1117

1218
[Unreleased]: https://github.com/envato/double_entry/compare/v2.0.1...HEAD
1319
[#225]: https://github.com/envato/double_entry/pull/225
20+
[#226]: https://github.com/envato/double_entry/pull/226
1421

1522
## [2.0.1] - 2023-11-01
1623

lib/double_entry/locking.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ def self.balance_for_locked_account(account)
5757
end
5858

5959
class Lock
60-
@@locks = {}
61-
6260
def initialize(accounts)
6361
# Make sure we always lock in the same order, to avoid deadlocks.
6462
@accounts = accounts.flatten.sort
@@ -97,15 +95,15 @@ def balance_for(account)
9795
private
9896

9997
def locks
100-
@@locks[Thread.current.object_id]
98+
Thread.current[:double_entry_locks]
10199
end
102100

103101
def locks=(locks)
104-
@@locks[Thread.current.object_id] = locks
102+
Thread.current[:double_entry_locks] = locks
105103
end
106104

107105
def remove_locks
108-
@@locks.delete(Thread.current.object_id)
106+
Thread.current[:double_entry_locks] = nil
109107
end
110108

111109
# Return true if there's a lock on the given account.

0 commit comments

Comments
 (0)