dalli 5.0.6 → 5.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/Gemfile +1 -1
- data/lib/dalli/client.rb +22 -3
- data/lib/dalli/protocol/meta.rb +1 -2
- data/lib/dalli/protocol/request_formatter.rb +22 -8
- data/lib/dalli/protocol/response_processor.rb +4 -2
- data/lib/dalli/socket.rb +2 -4
- data/lib/dalli/version.rb +1 -1
- data/lib/rack/session/dalli.rb +1 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da626a6bfcabdac3d5c8db15d06e6c567e7a302e6b479c2db3989d40a8e32d88
|
|
4
|
+
data.tar.gz: 1cdffab8886b14031f193af6e9dcd3cf26f463fc1612fb77c3ef2a8106ae41bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 871db8e41047a834823c5c2c1caf9c4bc47265ea8b37e48f54673c7350b719e74c34d70ce27103cef4bf6acd6fba85786052ce05a9234ae6f3e98e1d30725f70
|
|
7
|
+
data.tar.gz: 02c5b3085ca382fd612629af79a2d06f4dc9d960f579623a8af57f2a8e4332343467e886a12e73abe3b6c52a38504896e88e2d133eace4ec6f07e0ae68af8727
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.0.8
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
Bug fixes:
|
|
11
|
+
|
|
12
|
+
- Fix multi-server `get_multi` stopping at an empty value (#1170)
|
|
13
|
+
- The pipelined reply parser took a hit on a zero-length value (`VA 0`) for the terminating `MN`, so that server's remaining keys were silently missing from the result
|
|
14
|
+
- Its value terminator and any replies not yet parsed stayed on the connection and were read as the replies to later commands on it. Those commands could fail with `Dalli::DalliError: Response error`, or a single-key `get` could silently return another key's value
|
|
15
|
+
- An empty value is now returned as `''`, like a single-key `get` and single-server `get_multi`
|
|
16
|
+
- Affects the meta protocol since 3.2.0 (the default since 5.0.0)
|
|
17
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
18
|
+
|
|
19
|
+
Development:
|
|
20
|
+
|
|
21
|
+
- Fix offenses reported by RuboCop 1.91 and require `rubocop >= 1.91` (backport of #1162)
|
|
22
|
+
|
|
23
|
+
5.0.7
|
|
24
|
+
==========
|
|
25
|
+
|
|
26
|
+
Security:
|
|
27
|
+
|
|
28
|
+
- Fix memcached command injection through numeric arguments (GHSA-6wmv-xq9m-fmp7)
|
|
29
|
+
- The `default` argument of `incr`/`decr`, and `fetch_with_lock`'s `lock_ttl` and `recache_threshold`, were written into the meta protocol command without conversion, so a String containing CRLF injected additional memcached commands (e.g. `set`, `flush_all`) on the connection
|
|
30
|
+
- These arguments must now be Integers, or Strings of decimal digits; anything else raises `ArgumentError` before a request is sent
|
|
31
|
+
- As defense in depth, `RequestFormatter` now converts every numeric flag it writes (`D`, `J`, `N`, `R`, `T`) to an Integer
|
|
32
|
+
- Thanks to oss-security-shop for the report
|
|
33
|
+
|
|
7
34
|
5.0.6
|
|
8
35
|
==========
|
|
9
36
|
|
data/Gemfile
CHANGED
data/lib/dalli/client.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'digest/md5'
|
|
4
4
|
|
|
5
|
-
# encoding: ascii
|
|
6
5
|
module Dalli
|
|
7
6
|
##
|
|
8
7
|
# Dalli::Client is the main class which developers will use to interact with
|
|
@@ -144,7 +143,7 @@ module Dalli
|
|
|
144
143
|
# Fetch multiple keys efficiently.
|
|
145
144
|
# If a block is given, yields key/value pairs one at a time.
|
|
146
145
|
# Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
|
147
|
-
# rubocop:disable Style/ExplicitBlockArgument
|
|
146
|
+
# rubocop:disable-next Style/ExplicitBlockArgument
|
|
148
147
|
def get_multi(*keys)
|
|
149
148
|
keys.flatten!
|
|
150
149
|
keys.compact!
|
|
@@ -156,7 +155,6 @@ module Dalli
|
|
|
156
155
|
get_multi_hash(keys)
|
|
157
156
|
end
|
|
158
157
|
end
|
|
159
|
-
# rubocop:enable Style/ExplicitBlockArgument
|
|
160
158
|
|
|
161
159
|
##
|
|
162
160
|
# Fetch multiple keys efficiently, including available metadata such as CAS.
|
|
@@ -225,6 +223,8 @@ module Dalli
|
|
|
225
223
|
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
|
|
226
224
|
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
227
225
|
|
|
226
|
+
validate_integer!(:lock_ttl, lock_ttl)
|
|
227
|
+
validate_integer!(:recache_threshold, recache_threshold)
|
|
228
228
|
key = key.to_s
|
|
229
229
|
key = @key_manager.validate_key(key)
|
|
230
230
|
|
|
@@ -400,6 +400,8 @@ module Dalli
|
|
|
400
400
|
##
|
|
401
401
|
# Incr adds the given amount to the counter on the memcached server.
|
|
402
402
|
# Amt must be a positive integer value.
|
|
403
|
+
# Default, if given, must be an Integer (or a String of decimal digits);
|
|
404
|
+
# anything else raises ArgumentError.
|
|
403
405
|
#
|
|
404
406
|
# If default is nil, the counter must already exist or the operation
|
|
405
407
|
# will fail and will return nil. Otherwise this method will return
|
|
@@ -412,6 +414,7 @@ module Dalli
|
|
|
412
414
|
# If the value already exists, it must have been set with raw: true
|
|
413
415
|
def incr(key, amt = 1, ttl = nil, default = nil)
|
|
414
416
|
check_positive!(amt)
|
|
417
|
+
validate_integer!(:default, default)
|
|
415
418
|
|
|
416
419
|
perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
|
|
417
420
|
end
|
|
@@ -419,6 +422,8 @@ module Dalli
|
|
|
419
422
|
##
|
|
420
423
|
# Decr subtracts the given amount from the counter on the memcached server.
|
|
421
424
|
# Amt must be a positive integer value.
|
|
425
|
+
# Default, if given, must be an Integer (or a String of decimal digits);
|
|
426
|
+
# anything else raises ArgumentError.
|
|
422
427
|
#
|
|
423
428
|
# memcached counters are unsigned and cannot hold negative values. Calling
|
|
424
429
|
# decr on a counter which is 0 will just return 0.
|
|
@@ -434,6 +439,7 @@ module Dalli
|
|
|
434
439
|
# If the value already exists, it must have been set with raw: true
|
|
435
440
|
def decr(key, amt = 1, ttl = nil, default = nil)
|
|
436
441
|
check_positive!(amt)
|
|
442
|
+
validate_integer!(:default, default)
|
|
437
443
|
|
|
438
444
|
perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
|
|
439
445
|
end
|
|
@@ -617,6 +623,19 @@ module Dalli
|
|
|
617
623
|
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
|
618
624
|
end
|
|
619
625
|
|
|
626
|
+
# Numeric arguments that become meta protocol flags (GHSA-6wmv-xq9m-fmp7).
|
|
627
|
+
# RequestFormatter converts them to Integer as the wire-level backstop;
|
|
628
|
+
# checking here too raises a clean ArgumentError before the request
|
|
629
|
+
# starts, instead of Protocol::Base#request logging it as unexpected and
|
|
630
|
+
# closing the connection.
|
|
631
|
+
def validate_integer!(name, value)
|
|
632
|
+
return if value.nil?
|
|
633
|
+
|
|
634
|
+
value.is_a?(String) ? Integer(value, 10) : Integer(value)
|
|
635
|
+
rescue ArgumentError, TypeError, FloatDomainError
|
|
636
|
+
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
637
|
+
end
|
|
638
|
+
|
|
620
639
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
621
640
|
(value, cas) = perform(:cas, key)
|
|
622
641
|
return if value.nil? && !always_set
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -153,7 +153,7 @@ module Dalli
|
|
|
153
153
|
response_processor.meta_set_append_prepend unless quiet?
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
-
# rubocop:disable Metrics/ParameterLists
|
|
156
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
157
157
|
def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, _options = {})
|
|
158
158
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
159
159
|
req = RequestFormatter.meta_set(key: key, value: value,
|
|
@@ -161,7 +161,6 @@ module Dalli
|
|
|
161
161
|
write("#{req}#{value}#{TERMINATOR}")
|
|
162
162
|
@connection_manager.flush unless quiet?
|
|
163
163
|
end
|
|
164
|
-
# rubocop:enable Metrics/ParameterLists
|
|
165
164
|
|
|
166
165
|
# Delete Commands
|
|
167
166
|
def delete(key, cas)
|
|
@@ -43,10 +43,11 @@ module Dalli
|
|
|
43
43
|
# This saves 2 bytes per request and skips parsing on response.
|
|
44
44
|
cmd << (skip_flags ? ' v' : ' v f') if value
|
|
45
45
|
cmd << ' c' if return_cas
|
|
46
|
-
cmd << " T#{ttl}" if ttl
|
|
46
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
47
47
|
cmd << ' k q s' if quiet # Return the key in the response if quiet
|
|
48
|
-
cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
|
|
49
|
-
|
|
48
|
+
cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
|
|
49
|
+
# Thundering herd: win recache if TTL below threshold
|
|
50
|
+
cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
|
|
50
51
|
cmd << ' h' if return_hit_status # Return hit status (0 or 1)
|
|
51
52
|
cmd << ' l' if return_last_access # Return seconds since last access
|
|
52
53
|
cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
|
|
@@ -74,13 +75,14 @@ module Dalli
|
|
|
74
75
|
cmd << ' b' if base64
|
|
75
76
|
cmd << " F#{bitflags}" if bitflags
|
|
76
77
|
cmd << cas_string(cas)
|
|
77
|
-
cmd << " T#{ttl}" if ttl
|
|
78
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
78
79
|
cmd << " M#{mode_to_token(mode)}"
|
|
79
80
|
cmd << ' q' if quiet
|
|
80
81
|
cmd << TERMINATOR
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def multi_meta_set(entries, ttl: nil)
|
|
85
|
+
ttl = integer_flag(:ttl, ttl) if ttl
|
|
84
86
|
buffer = ''.b
|
|
85
87
|
entries.each do |key, pair|
|
|
86
88
|
value, bitflags = pair
|
|
@@ -104,7 +106,7 @@ module Dalli
|
|
|
104
106
|
def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false)
|
|
105
107
|
cmd = "md #{encoded_key(key)}"
|
|
106
108
|
cmd << cas_string(cas)
|
|
107
|
-
cmd << " T#{ttl}" if ttl
|
|
109
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
108
110
|
cmd << ' I' if stale # Mark stale instead of deleting
|
|
109
111
|
cmd << ' q' if quiet
|
|
110
112
|
cmd << TERMINATOR
|
|
@@ -120,10 +122,10 @@ module Dalli
|
|
|
120
122
|
|
|
121
123
|
def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false)
|
|
122
124
|
cmd = "ma #{encoded_key(key)} v"
|
|
123
|
-
cmd << " D#{delta}" if delta
|
|
124
|
-
cmd << " J#{initial}" if initial
|
|
125
|
+
cmd << " D#{integer_flag(:delta, delta)}" if delta
|
|
126
|
+
cmd << " J#{integer_flag(:initial, initial)}" if initial
|
|
125
127
|
# Always set a TTL if an initial value is specified
|
|
126
|
-
cmd << " N#{ttl
|
|
128
|
+
cmd << " N#{ttl ? integer_flag(:ttl, ttl) : 0}" if ttl || initial
|
|
127
129
|
cmd << cas_string(cas)
|
|
128
130
|
cmd << ' q' if quiet
|
|
129
131
|
cmd << " M#{incr ? 'I' : 'D'}"
|
|
@@ -169,6 +171,18 @@ module Dalli
|
|
|
169
171
|
|
|
170
172
|
private
|
|
171
173
|
|
|
174
|
+
# Numeric flag values are written straight into the command line, so a
|
|
175
|
+
# value that isn't an integer -- e.g. a String carrying CRLF -- would let
|
|
176
|
+
# the caller inject further memcached commands (GHSA-6wmv-xq9m-fmp7).
|
|
177
|
+
# Converting to Integer means only digits ever reach the wire. Strings
|
|
178
|
+
# are parsed as base 10 so "010" means 10, as memcached would read it,
|
|
179
|
+
# rather than octal 8.
|
|
180
|
+
def integer_flag(name, value)
|
|
181
|
+
value.is_a?(String) ? Integer(value, 10) : Integer(value)
|
|
182
|
+
rescue ArgumentError, TypeError, FloatDomainError
|
|
183
|
+
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
184
|
+
end
|
|
185
|
+
|
|
172
186
|
def mode_to_token(mode)
|
|
173
187
|
case mode
|
|
174
188
|
when :add
|
|
@@ -204,8 +204,10 @@ module Dalli
|
|
|
204
204
|
# We have a complete response that has no body.
|
|
205
205
|
# This is either the response to the terminating
|
|
206
206
|
# noop or, if the status is not MN, an intermediate
|
|
207
|
-
# error response that needs to be discarded.
|
|
208
|
-
|
|
207
|
+
# error response that needs to be discarded. A hit
|
|
208
|
+
# on an empty value (VA 0) still has a body -- just
|
|
209
|
+
# its terminator -- so it's parsed below as a value.
|
|
210
|
+
return [true, header_len] if body_len.zero? && tokens.first != VA
|
|
209
211
|
|
|
210
212
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
211
213
|
# The header is in the buffer, but the body is not. As we don't have
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -124,7 +124,7 @@ module Dalli
|
|
|
124
124
|
# Returns true for an unmodified TCPSocket on Ruby 3.0+, or for resolv-replace >= 0.2.0
|
|
125
125
|
# which forwards keyword arguments through its patch.
|
|
126
126
|
# Returns false when monkey-patched by gems like socksify or resolv-replace < 0.2.0.
|
|
127
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
127
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
128
128
|
def self.supports_connect_timeout?
|
|
129
129
|
return @supports_connect_timeout if defined?(@supports_connect_timeout)
|
|
130
130
|
|
|
@@ -135,7 +135,6 @@ module Dalli
|
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
|
-
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
139
138
|
|
|
140
139
|
def self.create_socket_with_timeout(host, port, options)
|
|
141
140
|
if supports_connect_timeout?
|
|
@@ -192,14 +191,13 @@ module Dalli
|
|
|
192
191
|
|
|
193
192
|
# Detect and cache the correct pack format for struct timeval on this platform.
|
|
194
193
|
# Different architectures have different sizes for time_t and suseconds_t.
|
|
195
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
194
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
196
195
|
def self.timeval_pack_format(sock)
|
|
197
196
|
@timeval_pack_format ||= begin
|
|
198
197
|
expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
|
|
199
198
|
TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
|
|
200
199
|
end
|
|
201
200
|
end
|
|
202
|
-
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
203
201
|
|
|
204
202
|
def self.pack_timeval(sock, seconds, microseconds)
|
|
205
203
|
[seconds, microseconds].pack(timeval_pack_format(sock))
|
data/lib/dalli/version.rb
CHANGED
data/lib/rack/session/dalli.rb
CHANGED
|
@@ -16,11 +16,10 @@ module Rack
|
|
|
16
16
|
attr_reader :data
|
|
17
17
|
|
|
18
18
|
# Don't freeze this until we fix the specs/implementation
|
|
19
|
-
# rubocop:disable Style/MutableConstant
|
|
19
|
+
# rubocop:disable-next Style/MutableConstant
|
|
20
20
|
DEFAULT_DALLI_OPTIONS = {
|
|
21
21
|
namespace: 'rack:session'
|
|
22
22
|
}
|
|
23
|
-
# rubocop:enable Style/MutableConstant
|
|
24
23
|
|
|
25
24
|
# Brings in a new Rack::Session::Dalli middleware with the given
|
|
26
25
|
# `:memcache_server`. The server is either a hostname, or a
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.0.
|
|
4
|
+
version: 5.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
88
|
version: '0'
|
|
89
89
|
requirements: []
|
|
90
|
-
rubygems_version: 4.0.
|
|
90
|
+
rubygems_version: 4.0.21
|
|
91
91
|
specification_version: 4
|
|
92
92
|
summary: High performance memcached client for Ruby
|
|
93
93
|
test_files: []
|