Skip to content

Commit aa98bc3

Browse files
Prevent CurrentAttributes defaults from leaking
Follow-up to rails#50677. Prior to this commit, all `ActiveSupport::CurrentAttributes` subclasses stored their default values in the same `Hash`, causing default values to leak between classes. This commit ensures each subclass maintains a separate `Hash`. This commit also simplifies the resolution of default values, replacing the `merge_defaults!` method with `resolve_defaults`.
1 parent c5f3a81 commit aa98bc3

File tree

2 files changed

+14
-13
lines changed

2 files changed

+14
-13
lines changed

activesupport/lib/active_support/current_attributes.rb

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def attribute(*names, default: nil)
134134

135135
singleton_class.delegate(*names.flat_map { |name| [name, "#{name}="] }, to: :instance, as: self)
136136

137-
defaults.merge! names.index_with { default }
137+
self.defaults = defaults.merge(names.index_with { default })
138138
end
139139

140140
# Calls this callback before #reset is called on the instance. Used for resetting external collaborators that depend on current values.
@@ -188,12 +188,12 @@ def method_added(name)
188188
end
189189
end
190190

191-
class_attribute :defaults, instance_writer: false, default: {}
191+
class_attribute :defaults, instance_writer: false, default: {}.freeze
192192

193193
attr_accessor :attributes
194194

195195
def initialize
196-
@attributes = merge_defaults!({})
196+
@attributes = resolve_defaults
197197
end
198198

199199
# Expose one or more attributes within a block. Old values are returned after the block concludes.
@@ -213,20 +213,14 @@ def set(attributes, &block)
213213
# Reset all attributes. Should be called before and after actions, when used as a per-request singleton.
214214
def reset
215215
run_callbacks :reset do
216-
self.attributes = merge_defaults!({})
216+
self.attributes = resolve_defaults
217217
end
218218
end
219219

220220
private
221-
def merge_defaults!(attributes)
222-
defaults.each_with_object(attributes) do |(name, default), values|
223-
value =
224-
case default
225-
when Proc then default.call
226-
else default.dup
227-
end
228-
229-
values[name] = value
221+
def resolve_defaults
222+
defaults.transform_values do |value|
223+
Proc === value ? value.call : value.dup
230224
end
231225
end
232226
end

activesupport/test/current_attributes_test.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,13 @@ def after_teardown
214214
assert_equal true, Current.respond_to?("respond_to_test")
215215
end
216216

217+
test "CurrentAttributes defaults do not leak between classes" do
218+
Class.new(ActiveSupport::CurrentAttributes) { attribute :counter_integer, default: 100 }
219+
Current.reset
220+
221+
assert_equal 0, Current.counter_integer
222+
end
223+
217224
test "CurrentAttributes use fiber-local variables" do
218225
previous_level = ActiveSupport::IsolatedExecutionState.isolation_level
219226
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber

0 commit comments

Comments
 (0)