dalli 5.2.3 → 5.2.4

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: 1a31a4159ee5cd637dca6676f343595f653798864bef28b109fbc329debe2aff
4
- data.tar.gz: efba0a1a4bb20eff2fd4f6b26c15359351484401502a798fa398b5bf1301d046
3
+ metadata.gz: 626c8dbb98798247423221395d6d518a87ad9ce64ce268488ecd4eb7d8d7f347
4
+ data.tar.gz: 999126be42f4c80be3eb566d3f10dfbb8e10b46446aaaf0da546523c0e734eef
5
5
  SHA512:
6
- metadata.gz: 38f6d3ba18790b94875f18bf6239a520f57144ba763826063471bea2047aa6def2cc528d37de35f29037a0c33ca9bb8b46d7ed0ddc5dccebf8914888c71dafd7
7
- data.tar.gz: 9f091240e20313b7cbb9250e900cbf3aa02f9aedbffceabeedc7355bc780cd3895e231c77bb091ab97680dbdac85e093635e3cfbca904fd7f095512181e4a59d
6
+ metadata.gz: 34aaa230697fd4ac27118ec8a90608d30bdc8dabaf9fc5cfc1b8c4e56a20c701708f1b6b7052fd76c7363f25e238905648296865c41b770f434457005af7aac8
7
+ data.tar.gz: 7bf1bbe810632bd9733a9cb122900ff9e1b283ade2ef071cec13d609cf4e0b2580de06f1116e9a71685fe98aa7804022c719ef3b921e41e9eb76f2be98b5ab1a
data/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 5.2.4
8
+ ==========
9
+
10
+ Security:
11
+
12
+ - Keep a connection shared by several threads or fibers in step during a `quiet` block (GHSA-5xvx-r8rg-5qm2)
13
+ - A `quiet` (or `multi`) block is quiet only for the thread or fiber that runs it. memcached still replies to a quiet request that fails (for example an `add` of a key that exists), and another thread or fiber using the same client while the block was open read that reply as its own, so its later requests could return another key's value. A non-quiet request now reads any pending quiet replies first
14
+ - Don't let a `get_multi` that stopped early leave replies for a later request (GHSA-5xvx-r8rg-5qm2)
15
+ - A pipelined `get_multi` stopped by a non-network exception (its block raising or breaking, `Timeout.timeout`, or a value that can't be deserialized) left its unread replies in the response buffer, and a later `get_multi` large enough to use the interleaved path returned them as its own. The buffer is now cleared when a connection closes or a pipelined get starts, and unfinished pipelines are aborted
16
+ - Affects every supported line (the `get_multi` issue, 4.3 and later); fixed in 5.2.4, 5.1.6, 5.0.12, 4.3.9 and 3.2.15. On 5.2, clients with `defer_drain: true` weren't affected by the `quiet` issue
17
+
18
+ Bug fixes:
19
+
20
+ - A failed TLS handshake is now handled as a network error even when openssl wasn't loaded before Dalli
21
+ - The fix in 5.2.3 (#1215) took effect only when the application had already required openssl: otherwise Dalli's list of TLS errors was empty, so a failed handshake still raised `OpenSSL::SSL::SSLError` and left the socket open
22
+
7
23
  5.2.3
8
24
  ==========
9
25
 
data/lib/dalli/client.rb CHANGED
@@ -434,6 +434,11 @@ module Dalli
434
434
  # the defer_drain option, before the next non-quiet request), and
435
435
  # Dalli client methods invoked inside the block will not
436
436
  # have return values
437
+ #
438
+ # The block is quiet only for the thread or fiber that runs it. Another
439
+ # thread or fiber sharing this client can make requests meanwhile; a
440
+ # non-quiet one first sends the block's buffered requests and discards
441
+ # their replies, so it reads only its own.
437
442
  def quiet
438
443
  old = Thread.current[::Dalli::QUIET]
439
444
  Thread.current[::Dalli::QUIET] = true
data/lib/dalli/options.rb CHANGED
@@ -43,7 +43,7 @@ module Dalli
43
43
  end
44
44
  end
45
45
 
46
- def pipeline_abort
46
+ def pipeline_abort(...)
47
47
  @lock.synchronize do
48
48
  super
49
49
  end
@@ -32,15 +32,21 @@ module Dalli
32
32
  return {} if keys.empty?
33
33
 
34
34
  @ring.lock do
35
- # Stores partial results collected during interleaved send phase
36
- @partial_results = {}
37
- servers = setup_requests(keys, req_options, return_cas: return_cas)
38
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
39
-
40
- # First yield any partial results collected during interleaved send
41
- yield_partial_results(&block)
42
-
43
- servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
35
+ # The servers with replies still to read, updated in place
36
+ servers = []
37
+ begin
38
+ # Stores partial results collected during interleaved send phase
39
+ @partial_results = {}
40
+ setup_requests(servers, keys, req_options, return_cas: return_cas)
41
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
42
+
43
+ # First yield any partial results collected during interleaved send
44
+ yield_partial_results(&block)
45
+
46
+ fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
47
+ ensure
48
+ abort_unfinished(servers)
49
+ end
44
50
  end
45
51
  rescue Dalli::RetryableNetworkError => e
46
52
  Dalli.logger.debug { e.inspect }
@@ -96,9 +102,9 @@ module Dalli
96
102
 
97
103
  # Sends each server's queries and its terminating noop before building
98
104
  # the next server's, so memcached is already answering the first servers
99
- # while the rest are prepared. Returns the servers with a pending response.
100
- def setup_requests(keys, req_options = nil, return_cas: true)
101
- started = []
105
+ # while the rest are prepared. Adds the servers with a pending response
106
+ # to started.
107
+ def setup_requests(started, keys, req_options = nil, return_cas: true)
102
108
  groups_for_keys(keys).each do |server, keys_for_server|
103
109
  make_getkq_request(server, keys_for_server, req_options, return_cas: return_cas)
104
110
  next unless server.connected?
@@ -111,7 +117,6 @@ module Dalli
111
117
  rescue Dalli::DalliError
112
118
  started.delete(server)
113
119
  end
114
- started
115
120
  end
116
121
 
117
122
  ##
@@ -155,29 +160,39 @@ module Dalli
155
160
  raise
156
161
  end
157
162
 
158
- # Swallows Dalli::NetworkError
163
+ # Swallows Dalli::NetworkError. Empties servers, so they aren't aborted
164
+ # again.
159
165
  def abort_without_timeout(servers)
160
166
  servers.each(&:pipeline_abort)
167
+ servers.clear
168
+ end
169
+
170
+ # When #process stops before reading every server's replies (the caller's
171
+ # block raises or breaks, a Timeout.timeout fires, a value can't be
172
+ # deserialized), the rest are still on the socket and some may be in the
173
+ # response buffer. Closing those connections keeps them from being read
174
+ # as a later request's replies. Aborts that #process already handled
175
+ # emptied servers.
176
+ def abort_unfinished(servers)
177
+ servers.each { |server| server.pipeline_abort(timed_out: false) }
178
+ servers.clear
161
179
  end
162
180
 
181
+ # Reads the replies available, removing finished servers from servers
163
182
  def fetch_responses(servers, start_time, timeout, &block)
164
183
  # Remove any servers which are not connected
165
184
  servers.select!(&:connected?)
166
- return [] if servers.empty?
185
+ return if servers.empty?
167
186
 
168
187
  time_left = remaining_time(start_time, timeout)
169
188
  readable_servers = servers_with_response(servers, time_left)
170
- if readable_servers.empty?
171
- abort_with_timeout(servers)
172
- return []
173
- end
189
+ return abort_with_timeout(servers) if readable_servers.empty?
174
190
 
175
191
  # Loop through the servers with responses, and
176
192
  # delete any from our list that are finished
177
193
  readable_servers.each do |server|
178
194
  servers.delete(server) if process_server(server, &block)
179
195
  end
180
- servers
181
196
  rescue NetworkError
182
197
  # Abort and raise if we encountered a network error. This triggers
183
198
  # a retry at the top level on RetryableNetworkError.
@@ -194,12 +209,10 @@ module Dalli
194
209
 
195
210
  # Swallows Dalli::NetworkError
196
211
  def abort_with_timeout(servers)
197
- abort_without_timeout(servers)
198
212
  servers.each do |server|
199
213
  Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
200
214
  end
201
-
202
- true # Required to simplify caller
215
+ abort_without_timeout(servers)
203
216
  end
204
217
 
205
218
  # Processes responses from a server. Returns true if there are no
@@ -69,7 +69,7 @@ module Dalli
69
69
  begin
70
70
  request_completed = false
71
71
  @connection_manager.start_request!
72
- drain_deferred_quiet_responses(opkey)
72
+ drain_pending_quiet_responses(opkey)
73
73
  response = send(opkey, *args)
74
74
  # Quiet requests can leave replies on the socket; remember that so the
75
75
  # end of the quiet block knows this server needs draining
@@ -147,7 +147,7 @@ module Dalli
147
147
  verify_pipelined_state(:getkq)
148
148
  write_noop
149
149
  # Use ensure_ready instead of reset to preserve any data already buffered
150
- # during interleaved pipelined get draining
150
+ # while this get_multi's interleaved requests were sent
151
151
  response_buffer.ensure_ready
152
152
  end
153
153
 
@@ -201,6 +201,10 @@ module Dalli
201
201
  end
202
202
  end
203
203
 
204
+ # A request to this server from inside the block closes the
205
+ # connection, and with it the rest of this get_multi's replies
206
+ raise_pipeline_closed unless response_buffer.in_progress?
207
+
204
208
  # Get the next response from the buffer
205
209
  status, cas, key, value = response_buffer.process_single_getk_response
206
210
  end
@@ -210,21 +214,24 @@ module Dalli
210
214
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
211
215
 
212
216
  # Abort current pipelined get. Generally used to signal an external
213
- # timeout during pipelined get. The underlying socket is
217
+ # timeout during pipelined get, or that the get_multi stopped before
218
+ # reading all its replies (timed_out: false). The underlying socket is
214
219
  # disconnected, and the exception is swallowed.
215
220
  #
216
221
  # Returns nothing.
217
- def pipeline_abort
222
+ def pipeline_abort(timed_out: true)
218
223
  response_buffer.clear
224
+ @pipeline_out_of_step = false
225
+ @expected_reply_keys = nil
219
226
  @connection_manager.abort_request!
220
227
  return true unless connected?
221
228
 
222
229
  # Closes the connection, which ensures that our connection is in a
223
230
  # clean state for future requests. The get_multi ran out of time
224
- # overall rather than hitting a socket error, so it doesn't count
225
- # toward socket_max_failures: two slow get_multis in a row would
226
- # otherwise mark a healthy server down.
227
- Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
231
+ # overall, or stopped early, rather than hitting a socket error, so it
232
+ # doesn't count toward socket_max_failures: two slow get_multis in a
233
+ # row would otherwise mark a healthy server down.
234
+ log_pipeline_abort(timed_out)
228
235
  @connection_manager.close
229
236
  true
230
237
  end
@@ -256,10 +263,15 @@ module Dalli
256
263
  ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
257
264
  private_constant :ALLOWED_QUIET_OPS
258
265
 
259
- # With defer_drain, quiet blocks leave their replies unread. Read them
260
- # before any non-quiet request, which reads a reply of its own.
261
- def drain_deferred_quiet_responses(opkey)
262
- return unless @defer_drain && !quiet? && opkey != :noop && @connection_manager.quiet_responses_pending?
266
+ # Reads the replies still pending from quiet requests before any
267
+ # non-quiet request, which reads a reply of its own. With defer_drain,
268
+ # quiet blocks leave their replies unread. Without it, a quiet block
269
+ # reads them at its end, but the connection is shared: another thread or
270
+ # fiber (quiet? is fiber-local) can make a request while the block is
271
+ # still open, and would otherwise read the block's replies as its own.
272
+ # The noop also sends any quiet requests still buffered.
273
+ def drain_pending_quiet_responses(opkey)
274
+ return unless @connection_manager.quiet_responses_pending? && opkey != :noop && !quiet?
263
275
 
264
276
  noop
265
277
  end
@@ -372,8 +384,9 @@ module Dalli
372
384
  # This prevents socket buffer deadlock when sending many keys.
373
385
  # Populates the provided results hash with any responses drained during send.
374
386
  def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
375
- # Initialize the response buffer for draining during send phase
376
- response_buffer.ensure_ready
387
+ # Start with an empty buffer for draining during the send phase. Bytes
388
+ # left by an earlier get_multi that stopped early belong to its replies.
389
+ response_buffer.reset
377
390
  response_buffer.raw = raw_request?(options)
378
391
  expect_reply_keys(keys)
379
392
 
@@ -421,8 +434,12 @@ module Dalli
421
434
  nil
422
435
  end
423
436
 
437
+ # Registered with the connection manager, which clears it whenever the
438
+ # connection closes, so a new connection never parses an old one's bytes
424
439
  def response_buffer
425
- @response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
440
+ @response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor).tap do |buffer|
441
+ @connection_manager.response_buffer = buffer
442
+ end
426
443
  end
427
444
 
428
445
  # A multi-get only accepts replies for the keys it asked for. memcached
@@ -474,6 +491,19 @@ module Dalli
474
491
  @connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
475
492
  end
476
493
 
494
+ def raise_pipeline_closed
495
+ raise Dalli::DalliError, "The connection to #{name} closed while a get_multi was reading its replies " \
496
+ '(a request made from inside the get_multi block?)'
497
+ end
498
+
499
+ def log_pipeline_abort(timed_out)
500
+ if timed_out
501
+ Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
502
+ else
503
+ Dalli.logger.debug { "#{name} get_multi stopped before reading all replies; closing the connection" }
504
+ end
505
+ end
506
+
477
507
  # The single-server set_multi path passes its whole pairs hash as the
478
508
  # first argument; log only its keys, never the values
479
509
  def log_marshal_err(key, err)
@@ -26,6 +26,9 @@ module Dalli
26
26
 
27
27
  attr_accessor :hostname, :port, :socket_type, :options
28
28
  attr_reader :sock
29
+ # The buffer replies are read into (see Base#response_buffer). It's
30
+ # cleared with the write buffer whenever the connection closes.
31
+ attr_writer :response_buffer
29
32
 
30
33
  def initialize(hostname, port, socket_type, client_options)
31
34
  @hostname = hostname
@@ -38,6 +41,7 @@ module Dalli
38
41
  @pid = nil
39
42
  @write_buffer = []
40
43
  @write_buffer_bytes = 0
44
+ @response_buffer = nil
41
45
 
42
46
  @fail_count = 0
43
47
  reset_down_info
@@ -133,6 +137,7 @@ module Dalli
133
137
  @pid = nil
134
138
  @quiet_responses_pending = false
135
139
  discard_write_buffer
140
+ @response_buffer&.clear
136
141
  abort_request!
137
142
  end
138
143
  end
@@ -283,10 +283,12 @@ module Dalli
283
283
  # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
284
284
  # We need to read all the responses at once.
285
285
  # A write that expects a reply is flushed so the reply can be read. A write
286
- # in a quiet block stays buffered until the block's noop, unless
287
- # defer_drain is on: then no noop follows at the end of the block, so it is
288
- # flushed right away. (pipelined_set passes quiet: true outside a quiet
289
- # block; its caller flushes with a terminating noop.)
286
+ # in a quiet block stays buffered until a noop: the one at the end of the
287
+ # block, or the one another thread or fiber sends to drain the block's
288
+ # replies before its own request. With defer_drain no noop follows at the
289
+ # end of the block, so it is flushed right away. (pipelined_set passes
290
+ # quiet: true outside a quiet block; its caller flushes with a
291
+ # terminating noop.)
290
292
  def finish_write(quiet)
291
293
  @connection_manager.flush if !quiet || (@defer_drain && quiet?)
292
294
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'openssl'
3
4
  require 'timeout'
4
5
 
5
6
  module Dalli
@@ -19,11 +20,8 @@ module Dalli
19
20
  # SSL errors during the handshake or during read/write operations count as
20
21
  # a failed request: they trigger reconnection and, after
21
22
  # socket_max_failures, mark the server down.
22
- SSL_ERRORS =
23
- if defined?(OpenSSL::SSL::SSLError)
24
- [OpenSSL::SSL::SSLError]
25
- else
26
- []
27
- end
23
+ # openssl is required above: this file loads before dalli/socket, so a
24
+ # defined? check here would find OpenSSL missing and leave this empty.
25
+ SSL_ERRORS = [OpenSSL::SSL::SSLError].freeze
28
26
  end
29
27
  end
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '5.2.3'
4
+ VERSION = '5.2.4'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.6.27'
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: 5.2.3
4
+ version: 5.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein