dalli 4.3.7 → 4.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b1a68fbdf9ea89024d6e9bc5803f94d3342c2f7b8bd7cfea3a97f7a873f7b76
4
- data.tar.gz: 93d1c2fcb71fa739e6a00808cddb11567ad9fed3caa4d0751c924576bc04bfe0
3
+ metadata.gz: 7cddc5e9ac0bd88077676d94695d72804971c1b5a65522e3f8bff05b9b35f75a
4
+ data.tar.gz: 7c506e73dd7b0c71864aaa99bb35537642dd966ad692101556be733050891c1b
5
5
  SHA512:
6
- metadata.gz: bc42cde8b6f61ba60768bf43a9ebf8b1088dc8b2efbb4e32b5ed1abedd3c3be81c66aa02dc1a2558fd319debcc8074e61471996ad8825afed32d4fd741f8a312
7
- data.tar.gz: 3a8320685a39edef196feaa387ef954caea41650b540c7f37d32d4821b5804f41631e5346e14b21912de0963f147c7d55e8c1ad08e24d2f7d25073975ba64d72
6
+ metadata.gz: db49da025cb40d2902dbd394d9c2ee1f3839e41b912f1e3d8a2f3f0f096e409e4ea564a8f9783677b00b5307aef4c64951818b385af66d8c958103a36e89f076
7
+ data.tar.gz: a8d55c26b215141a5af0f59782e1e8a3443f07b73a27175bc39cde8bda798c176e88c658b8cebad14c52198d974fd30ae6e859f321102cdf6d738b7157fc5bbe
data/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 4.3.8
8
+ ==========
9
+
10
+ Security:
11
+
12
+ - Don't rely on memcached's auto-create for keys that need base64 encoding (GHSA-r47x-6p6r-mvcp)
13
+ - memcached stores an item made by auto-create (`ma` with `N`/`J`, or `mg` with `N`) for a base64-encoded key without its binary-key flag, and later replies name the item by its raw key bytes. A key containing spaces or CR/LF could then make a multi-get read the replies after it as different ones
14
+ - `incr` and `decr` with an initial value now create such counters with an add instead. Return values are unchanged
15
+ - The new `verify_reply_keys` option makes `get_multi` and `get_multi_cas` ignore replies for keys they didn't request, which guards against items created by older Dalli versions or other clients until memcached is upgraded. It's off by default, since it costs a multi-get about 15%
16
+ - Affects `protocol: :meta` since 3.2.0; fixed in 5.2.3, 5.1.5, 5.0.11, 4.3.8 and 3.2.14. memcached 1.6.45 and earlier still name these items by their raw key bytes
17
+
7
18
  4.3.7
8
19
  ==========
9
20
 
data/README.md CHANGED
@@ -82,6 +82,10 @@ Per-request `raw: true` returns the stored bytes without deserializing or decomp
82
82
 
83
83
  See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
84
84
 
85
+ ### Counters and verify_reply_keys
86
+
87
+ memcached names an item made by auto-create for a base64-encoded key (one with whitespace, control or non-ASCII characters) by its raw key bytes in later replies (with `protocol: :meta`). Since 4.3.8, Dalli creates counters for such keys with an add rather than auto-create. Items created before then, or by other clients, can still be named that way until memcached releases a fix (1.6.45 and earlier behave this way). If keys passed to `incr` or `decr` with an initial value can contain untrusted input, set `verify_reply_keys: true` until then: multi-gets then ignore replies for keys they didn't request, at a cost of about 15% per multi-get.
88
+
85
89
  ## OpenTelemetry Tracing
86
90
 
87
91
  Dalli automatically instruments operations with [OpenTelemetry](https://opentelemetry.io/) when the SDK is present. No configuration is required - just add the OpenTelemetry gems to your application:
data/lib/dalli/client.rb CHANGED
@@ -53,6 +53,11 @@ module Dalli
53
53
  # +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
54
54
  # +nil+ (default) omits the attribute entirely.
55
55
  # - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
56
+ # - :verify_reply_keys - defaults to false. When true, get_multi and get_multi_cas only accept replies for the keys
57
+ # they requested; after a reply naming another key, the rest of that server's replies are
58
+ # treated as misses. With the meta protocol, memcached can name an item by its raw key bytes
59
+ # when the item was created by auto-create from a base64-encoded key, which Dalli no longer
60
+ # does. Costs a multi-get about 15%.
56
61
  #
57
62
  def initialize(servers = nil, options = {})
58
63
  @normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
+ require 'set'
4
5
  require 'socket'
5
6
  require 'timeout'
6
7
 
@@ -24,6 +25,7 @@ module Dalli
24
25
  hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
25
26
  @options = client_options.merge(user_creds)
26
27
  @raw_mode = client_options[:raw]
28
+ @verify_reply_keys = client_options[:verify_reply_keys] ? true : false
27
29
  @value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
28
30
  @connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
29
31
  end
@@ -103,6 +105,8 @@ module Dalli
103
105
  # Without a block, returns a Hash of { key => [value, cas] }.
104
106
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
105
107
  def pipeline_next_responses(&block)
108
+ return abandon_pipeline if @pipeline_out_of_step
109
+
106
110
  reconnect_on_pipeline_complete!
107
111
  values = nil
108
112
 
@@ -124,6 +128,13 @@ module Dalli
124
128
  # to the noop at the end of the pipeline
125
129
  finish_pipeline && break if status && key.nil?
126
130
 
131
+ # A key that wasn't requested means the replies are out of step
132
+ # (see expect_reply_keys): keep what was read so far, drop the rest
133
+ if unexpected_reply_key?(key)
134
+ abandon_pipeline
135
+ break
136
+ end
137
+
127
138
  # If the status is ok and the key is not nil, then this is a
128
139
  # getkq response with a value that we want to set in the response hash
129
140
  unless key.nil?
@@ -252,6 +263,7 @@ module Dalli
252
263
  # Use clear (not reset) to keep pipeline_complete? = true, which is
253
264
  # the expected state before pipeline_response_setup is called.
254
265
  response_buffer.clear
266
+ expect_reply_keys(keys)
255
267
 
256
268
  req = +''
257
269
  keys.each do |key|
@@ -267,6 +279,7 @@ module Dalli
267
279
  def pipelined_get_interleaved(keys, chunk_size, results)
268
280
  # Initialize the response buffer for draining during send phase
269
281
  response_buffer.ensure_ready
282
+ expect_reply_keys(keys)
270
283
 
271
284
  keys.each_slice(chunk_size) do |chunk|
272
285
  # Build and write this chunk of requests
@@ -299,6 +312,12 @@ module Dalli
299
312
  status, cas, key, value = response_buffer.process_single_getk_response
300
313
  break if status.nil? # No complete response available
301
314
 
315
+ # Handled by the next pipeline_next_responses, once sending is done
316
+ if unexpected_reply_key?(key)
317
+ @pipeline_out_of_step = true
318
+ break
319
+ end
320
+
302
321
  results[key] = [value, cas] unless key.nil?
303
322
  end
304
323
  rescue SystemCallError, Dalli::NetworkError
@@ -310,6 +329,42 @@ module Dalli
310
329
  @response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
311
330
  end
312
331
 
332
+ # A multi-get only accepts replies for the keys it asked for. memcached
333
+ # names an item created by auto-create (ma or mg with N) for a
334
+ # base64-encoded key by its raw bytes, without the b flag; such a key
335
+ # can contain spaces or CR/LF, which split the reply header and make the
336
+ # replies after it parse as different ones. A key that wasn't requested
337
+ # is how that shows up.
338
+ # Only with the verify_reply_keys option, since building the set costs a
339
+ # multi-get about 15%. Dalli itself no longer creates such items (see
340
+ # Meta#decr_incr); the check guards against ones that older versions or
341
+ # other clients created, on memcached versions with the behavior.
342
+ def expect_reply_keys(keys)
343
+ @pipeline_out_of_step = false
344
+ @expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
345
+ end
346
+
347
+ def unexpected_reply_key?(key)
348
+ !key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
349
+ end
350
+
351
+ # Keys compared as bytes; ASCII keys compare equal in any encoding as is
352
+ def reply_key_form(key)
353
+ key.ascii_only? ? key : key.b
354
+ end
355
+
356
+ # Keeps what was read, drops the rest of this server's replies, and
357
+ # closes the connection so nothing unread is left on it. Not a socket
358
+ # failure, so it doesn't count toward socket_max_failures; the keys not
359
+ # read are simply missing from the result.
360
+ def abandon_pipeline
361
+ Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
362
+ @pipeline_out_of_step = false
363
+ response_buffer.clear
364
+ @connection_manager.close
365
+ {}
366
+ end
367
+
313
368
  # Called after the noop response is received at the end of a set
314
369
  # of pipelined gets
315
370
  def finish_pipeline
@@ -213,12 +213,45 @@ module Dalli
213
213
  def decr_incr(incr, key, delta, ttl, initial)
214
214
  ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
215
215
  encoded_key, base64 = KeyRegularizer.encode(key)
216
+ # memcached stores an item created by ma's auto-create (N/J) for a
217
+ # base64-encoded key without its binary-key flag, and later replies
218
+ # name it with the raw key bytes. Create such counters with an add.
219
+ return decr_incr_with_add(incr, key, delta, ttl, initial) if initial && base64
220
+
216
221
  write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
217
222
  quiet: quiet?, base64: base64))
218
223
  @connection_manager.flush unless quiet?
219
224
  response_processor.decr_incr unless quiet?
220
225
  end
221
226
 
227
+ # Like decr_incr with an initial value, without memcached's auto-create:
228
+ # on a miss the counter is added with the initial value (which is
229
+ # returned, as auto-create would), and the add losing a race to another
230
+ # client means the counter now exists, so the incr is tried again. In a
231
+ # quiet block both requests are sent and whichever applies takes effect.
232
+ def decr_incr_with_add(incr, key, delta, ttl, initial)
233
+ add_options = { raw: true } # counters are stored raw
234
+ return quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options) if quiet?
235
+
236
+ 2.times do
237
+ value = decr_incr(incr, key, delta, nil, nil)
238
+ return value unless value.nil?
239
+
240
+ write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: false)
241
+ return initial if response_processor.meta_set_with_cas
242
+ end
243
+ decr_incr(incr, key, delta, nil, nil)
244
+ end
245
+
246
+ # rubocop:disable-next Metrics/ParameterLists
247
+ def quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options)
248
+ encoded_key, base64 = KeyRegularizer.encode(key)
249
+ write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: nil, incr: incr, ttl: nil,
250
+ quiet: true, base64: base64))
251
+ # Sent with the rest of the quiet block, by its closing noop
252
+ write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: true)
253
+ end
254
+
222
255
  # Other Commands
223
256
  def flush(delay = 0)
224
257
  write(RequestFormatter.flush(delay: delay))
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '4.3.7'
4
+ VERSION = '4.3.8'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
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: 4.3.7
4
+ version: 4.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -94,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0'
96
96
  requirements: []
97
- rubygems_version: 4.0.21
97
+ rubygems_version: 4.0.23
98
98
  specification_version: 4
99
99
  summary: High performance memcached client for Ruby
100
100
  test_files: []