dalli 3.2.12 → 3.2.13

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: f8f2b8a277c11f36013ba0a3997f962e38a9ce19a4dbe05dfc42a647016d03e0
4
- data.tar.gz: 19945129c63f04cf6dc704063eb31bd243998a987b8f8b729345d5b554d2ca36
3
+ metadata.gz: 7f7bcbe37e631c3c733d1487e519501497702b873e81e6588d6dfad1ab96a65b
4
+ data.tar.gz: d7a953d2c0052334d8edb7fa45306724ffa25cec64436dcceccc9847d057e8bd
5
5
  SHA512:
6
- metadata.gz: 3cc736edcbbcb13054fba7ace600d0e88b4b6e323b84ee11446dd57071b6f9a8db525050b0a7b3c8166c943efb042d791dc62a38f930bed7a5b818ad96fc9152
7
- data.tar.gz: db46135255631a70767867449b0ed61b14d59ce2a8e8b65fe809a17f660a8d4f46a5c62197fa8d408371f7697711d4fae92031721e13809519b52a94427dd380
6
+ metadata.gz: 8d7b0ed94102d333f611736c48d944b0f1010760ae646003db60cd02c90b1a047a5fd10f7a5ef077527a88594f8844cbe08f9647259749e78ade74d09d2e851b
7
+ data.tar.gz: bf86e4604fc18eaf2bdf57d6abad82258745238112a9866e5da9e05c0f45b44d62f82ebe8b5519ecc83d00ec6ee94e2d11bb931aa14171081a0b731745633e6d
data/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 3.2.13
8
+ ==========
9
+
10
+ - Complete the fix for per-request `raw: true` on reads (GHSA-wr87-m4jw-29x5)
11
+ - `cas` and `cas!` accepted `raw: true` but didn't pass it to the read, so the value was still deserialized. They now honor it, in both protocols
12
+ - Affects 3.2.12 and earlier; fixed in 5.2.2, 5.1.4, 5.0.10, 4.3.7 and 3.2.13
13
+ - Complete the fork-safety fix (GHSA-w39f-xq2m-4g8x)
14
+ - Fork detection used a pid cache refreshed by a `Process._fork` hook. When another library's fork hook ran first in the child (connection_pool, loaded before Dalli as in Rails, closes its connections there), Dalli didn't know it was in a forked child and ended the parent's TLS session. It now checks `Process.pid`
15
+ - Fixed in 4.3.7 and 3.2.13
16
+
17
+ - Fix a regression in 3.2.12: a `get_multi` that didn't finish within `socket_timeout` counted toward `socket_max_failures`, so two slow `get_multi` calls in a row marked a healthy server down. It now just closes the connection
18
+ - Handle malformed pipelined replies from a broken or hostile server or proxy: a hit with no key, or with no `s` flag, could leave the connection out of step or raise an error
19
+ - With `decompressed_max_bytes` in effect, data after the end of a compressed value's stream was returned as part of the value. It's now ignored, as without the limit
20
+ - With a `digest_class` whose digests aren't short hex strings, shortening a long key could loop forever. It now raises `ArgumentError`
21
+
22
+ - Run the Tests and RuboCop workflows on pushes to `main` and the `*-stable` branches only, so a pull request's branch isn't tested twice (backport of #1198)
23
+
7
24
  3.2.12
8
25
  ==========
9
26
 
data/lib/dalli/client.rb CHANGED
@@ -388,7 +388,8 @@ module Dalli
388
388
  end
389
389
 
390
390
  def cas_core(key, always_set, ttl = nil, req_options = nil)
391
- (value, cas) = perform(:cas, key)
391
+ # req_options reaches the read too, so raw: true returns the stored bytes
392
+ (value, cas) = perform(:cas, key, req_options)
392
393
  return if value.nil? && !always_set
393
394
 
394
395
  newvalue = yield(value)
@@ -39,7 +39,12 @@ module Dalli
39
39
  out = String.new(encoding: Encoding::BINARY)
40
40
  0.step(data.bytesize - 1, INFLATE_SLICE_BYTES) do |pos|
41
41
  inflate_slice(inflater, data.byteslice(pos, INFLATE_SLICE_BYTES), out, max_bytes)
42
+ # Anything after the end of the stream isn't part of the value, as
43
+ # with Zlib::Inflate.inflate; finish would return it as output
44
+ break if inflater.finished?
42
45
  end
46
+ return out if inflater.finished?
47
+
43
48
  append_within_limit(out, inflater.finish, max_bytes)
44
49
  ensure
45
50
  inflater.close
@@ -110,15 +110,18 @@ module Dalli
110
110
  ##
111
111
  def truncated_key(key)
112
112
  digest = digest_class.hexdigest(key)
113
- prefix = key[0, prefix_length(digest)]
113
+ prefix = key[0, [prefix_length(digest), 0].max]
114
114
  truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
115
115
  # A prefix with whitespace or non-ASCII characters sends the whole key
116
116
  # base64-encoded, which can still be too long on the wire; shorten it.
117
- while wire_length(truncated) > MAX_KEY_LENGTH
117
+ while wire_length(truncated) > MAX_KEY_LENGTH && !prefix.empty?
118
118
  prefix = prefix[0...-1]
119
119
  truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
120
120
  end
121
- truncated
121
+ return truncated if wire_length(truncated) <= MAX_KEY_LENGTH
122
+
123
+ # Only possible with a digest_class whose digests aren't short hex strings
124
+ raise ArgumentError, "#{digest_class} digests are too long to fit in a truncated key"
122
125
  end
123
126
 
124
127
  # memcached's key limit applies to the key as sent: in bytes, and, with
@@ -124,13 +124,16 @@ module Dalli
124
124
  def pipeline_abort
125
125
  response_buffer.clear
126
126
  @connection_manager.abort_request!
127
- return true unless connected?
128
-
129
- # Closes the connection, which ensures that our connection
130
- # is in a clean state for future requests
131
- @connection_manager.error_on_request!('External timeout')
132
- rescue NetworkError
133
- true
127
+ return unless connected?
128
+
129
+ # Closes the connection, which ensures that our connection is in a
130
+ # clean state for future requests. The get_multi ran out of time
131
+ # overall rather than hitting a socket error, so it doesn't count
132
+ # toward socket_max_failures: two slow get_multis in a row would
133
+ # otherwise mark a healthy server down.
134
+ Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
135
+ @connection_manager.close
136
+ nil
134
137
  end
135
138
 
136
139
  # Did the last call to #pipeline_response_setup complete successfully?
@@ -102,7 +102,8 @@ module Dalli
102
102
  true
103
103
  end
104
104
 
105
- def data_cas_response
105
+ # raw: true returns the value as stored; see #get
106
+ def data_cas_response(raw: false)
106
107
  resp_header, body = read_response
107
108
  return [nil, resp_header.cas] if resp_header.not_found?
108
109
  return [nil, false] if resp_header.not_stored?
@@ -110,7 +111,7 @@ module Dalli
110
111
  raise_on_not_ok!(resp_header)
111
112
  return [nil, resp_header.cas] unless body
112
113
 
113
- [unpack_response_body(resp_header, body, true).last, resp_header.cas]
114
+ [unpack_response_body(resp_header, body, !raw).last, resp_header.cas]
114
115
  end
115
116
 
116
117
  # Returns the new value for the key, if found and updated
@@ -44,10 +44,10 @@ module Dalli
44
44
 
45
45
  # TODO: This is confusing, as there's a cas command in memcached
46
46
  # and this isn't it. Maybe rename? Maybe eliminate?
47
- def cas(key)
47
+ def cas(key, options = nil)
48
48
  req = RequestFormatter.standard_request(opkey: :get, key: key)
49
49
  write(req)
50
- response_processor.data_cas_response
50
+ response_processor.data_cas_response(raw: raw_request?(options))
51
51
  end
52
52
 
53
53
  # Storage Commands
@@ -54,7 +54,7 @@ module Dalli
54
54
  Dalli.logger.debug { "Dalli::Server#connect #{name}" }
55
55
 
56
56
  @sock = memcached_socket
57
- @pid = PIDCache.pid
57
+ @pid = Process.pid
58
58
  @request_in_progress = false
59
59
  rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
60
60
  # SocketError = DNS resolution failure
@@ -257,7 +257,11 @@ module Dalli
257
257
  end
258
258
 
259
259
  def fork_detected?
260
- @pid && @pid != PIDCache.pid
260
+ # Process.pid rather than PIDCache: PIDCache is refreshed by a
261
+ # Process._fork hook, and another library's fork hook (such as
262
+ # connection_pool closing its connections) can run in the child before
263
+ # it, which would close the parent's TLS session as if it were ours.
264
+ @pid && @pid != Process.pid
261
265
  end
262
266
 
263
267
  def log_down_detected
@@ -38,14 +38,16 @@ module Dalli
38
38
  raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens))
39
39
  end
40
40
 
41
- def meta_get_with_value_and_cas
41
+ # raw: true returns the value as stored; see #meta_get_with_value
42
+ def meta_get_with_value_and_cas(raw: false)
42
43
  tokens = error_on_unexpected!([VA, EN, HD])
43
44
  return [nil, 0] if tokens.first == EN
44
45
 
45
46
  cas = cas_from_tokens(tokens)
46
47
  return [nil, cas] unless tokens.first == VA
47
48
 
48
- [@value_marshaller.retrieve(read_line, bitflags_from_tokens(tokens)), cas]
49
+ value = read_line
50
+ [raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens)), cas]
49
51
  end
50
52
 
51
53
  def meta_get_without_value
@@ -123,6 +125,10 @@ module Dalli
123
125
  end
124
126
 
125
127
  def full_response_from_buffer(tokens, body, resp_size)
128
+ # A hit with no key can't be matched to a request: skip it (false
129
+ # status) rather than yield it, or take it for the end of the pipeline
130
+ return [resp_size, false, nil, nil, nil] unless tokens.any? { |t| t.start_with?('k') }
131
+
126
132
  value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
127
133
  [resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
128
134
  end
@@ -168,6 +174,11 @@ module Dalli
168
174
  # buffer wait for (and accumulate) that many bytes.
169
175
  MAX_VALUE_BYTES = 1024 * 1024 * 1024
170
176
 
177
+ # The size given right after "VA", or 0 when it isn't a number
178
+ def positional_size(tokens)
179
+ tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
180
+ end
181
+
171
182
  def check_value_size!(size)
172
183
  return if size.between?(0, MAX_VALUE_BYTES)
173
184
 
@@ -209,7 +220,9 @@ module Dalli
209
220
  end
210
221
 
211
222
  def body_len_from_tokens(tokens)
212
- size = value_from_tokens(tokens, 's')&.to_i
223
+ # memcached also gives the size positionally ("VA <size> ..."); use it
224
+ # when there's no s flag rather than reading the value as replies
225
+ size = tokens.any? { |t| t.start_with?('s') } ? value_from_tokens(tokens, 's')&.to_i : positional_size(tokens)
213
226
  check_value_size!(size) if size
214
227
  size
215
228
  end
@@ -53,11 +53,11 @@ module Dalli
53
53
 
54
54
  # TODO: This is confusing, as there's a cas command in memcached
55
55
  # and this isn't it. Maybe rename? Maybe eliminate?
56
- def cas(key)
56
+ def cas(key, options = nil)
57
57
  encoded_key, base64 = KeyRegularizer.encode(key)
58
58
  req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
59
59
  write(req)
60
- response_processor.meta_get_with_value_and_cas
60
+ response_processor.meta_get_with_value_and_cas(raw: raw_request?(options))
61
61
  end
62
62
 
63
63
  # Storage Commands
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '3.2.12'
4
+ VERSION = '3.2.13'
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: 3.2.12
4
+ version: 3.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein