Skip to content

Commit f4418db

Browse files
authored
Merge pull request rails#46048 from rails/fix-hash-collisions
Dup and freeze complex types when making query attributes
2 parents 25c3d42 + 5abb45d commit f4418db

File tree

5 files changed

+59
-1
lines changed

5 files changed

+59
-1
lines changed

activemodel/lib/active_model/type/helpers/mutable.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ module ActiveModel
44
module Type
55
module Helpers # :nodoc: all
66
module Mutable
7+
def immutable_value(value)
8+
value.deep_dup.freeze
9+
end
10+
711
def cast(value)
812
deserialize(serialize(value))
913
end

activemodel/lib/active_model/type/value.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ def hash
129129
def assert_valid_value(_)
130130
end
131131

132+
def immutable_value(value) # :nodoc:
133+
value
134+
end
135+
132136
private
133137
# Convenience method for types which do not need separate type casting
134138
# behavior for user and database inputs. Called by Value#cast for

activerecord/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Fix a case where the query cache can return wrong values. See #46044
2+
3+
*Aaron Patterson*
4+
15
* Support MySQL's ssl-mode option for MySQLDatabaseTasks.
26

37
Verifying the identity of the database server requires setting the ssl-mode

activerecord/lib/active_record/relation/predicate_builder.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ def build(attribute, value, operator = nil)
6565
end
6666

6767
def build_bind_attribute(column_name, value)
68-
Relation::QueryAttribute.new(column_name, value, table.type(column_name))
68+
type = table.type(column_name)
69+
Relation::QueryAttribute.new(column_name, type.immutable_value(value), type)
6970
end
7071

7172
def resolve_arel_attribute(table_name, column_name, &block)

activerecord/test/cases/query_cache_test.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,51 @@ def assert_cache(state, connection = ActiveRecord::Base.connection)
646646
end
647647
end
648648

649+
class QueryCacheMutableParamTest < ActiveRecord::TestCase
650+
self.use_transactional_tests = false
651+
652+
class JsonObj < ActiveRecord::Base
653+
self.table_name = "json_objs"
654+
655+
attribute :payload, :json
656+
end
657+
658+
class HashWithFixedHash < Hash
659+
# this isn't very realistic, but it is the worst case and therefore a good
660+
# case to test
661+
def hash
662+
1
663+
end
664+
end
665+
666+
def setup
667+
ActiveRecord::Base.connection.create_table("json_objs", force: true) do |t|
668+
if current_adapter?(:PostgreSQLAdapter)
669+
t.jsonb "payload"
670+
else
671+
t.json "payload"
672+
end
673+
end
674+
675+
ActiveRecord::Base.connection.enable_query_cache!
676+
end
677+
678+
def test_query_cache_handles_mutated_binds
679+
JsonObj.create(payload: { a: 1 })
680+
681+
search = HashWithFixedHash[a: 1]
682+
JsonObj.where(payload: search).first # populate the cache
683+
684+
search.merge!(b: 2)
685+
assert_nil JsonObj.where(payload: search).first, "cache returned a false positive"
686+
end
687+
688+
def teardown
689+
ActiveRecord::Base.connection.disable_query_cache!
690+
ActiveRecord::Base.connection.drop_table("json_objs", if_exists: true)
691+
end
692+
end
693+
649694
class QueryCacheExpiryTest < ActiveRecord::TestCase
650695
fixtures :tasks, :posts, :categories, :categories_posts
651696

0 commit comments

Comments
 (0)