fluentd 1.19.3-x64-mingw32 → 1.19.4-x64-mingw32

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: b02fab90cf21116c45ee6fc51eb3719946b0d6699092cc94adaf2ced11e8f631
4
- data.tar.gz: 9eb17482dcd40ef2f8211785e211be6fbcd4ec907a4c4f04ea8e58c3b52e2b6d
3
+ metadata.gz: b109ce733ba054b3b03daa1f47658e8287dcb3b2e0a1ca2c0cca4defb8b1415a
4
+ data.tar.gz: d80e36b87699060e8c895d9fb332dc521f0ba9cdbb0b0f4c80dd996003fdca50
5
5
  SHA512:
6
- metadata.gz: 395b0e0a4a8774d46014a3f47fff15c9c226d5d7547c22a3d90dcf309cbfbce55aa2e1dcf02160b6d6909f0aa8737f37a0c95a956c24c317dc8958e3db43de7a
7
- data.tar.gz: 7febff6dc16fae70f92d0b9855e851d8119e31e89dd79107a3a13bd32bfb7b07fbc28fcebbdbd081c7542fcb42c310215c7607b7fe6f6538d893a05a53054ada
6
+ metadata.gz: ec9c12b4dfa48360cd34ba349a84ab8777a079a1d8c35af4a28f94fd1db56691846cef5d6df96cd2ff690e1b467467b6f51c6672f956e90703bc02adb67654c1
7
+ data.tar.gz: 7af35f61b6d84663e4871f410e9f4d32116d59e5953ecf32797fbb3331f5caefff699d1a55820967b4b45d183ac0980a9776284e4d29ed94828264d5648d7992
data/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # v1.19
2
2
 
3
+ ## Release v1.19.4 - 2026/09/28
4
+
5
+ ### Bug Fix
6
+
7
+ * buffer: enforce decompression_size_limit on the chunk IO path https://github.com/fluent/fluentd/pull/5504
8
+ * buffer: clamp exported buffer size metrics to non-negative values https://github.com/fluent/fluentd/pull/5488
9
+ * buffer: fix spurious `BufferOverflowError` caused by `queue_size` leaking when a chunk purge fails https://github.com/fluent/fluentd/pull/5487
10
+ * buffer: fix `stage_byte_size` leak on staged to unstaged chunk demotion that could eventually raise spurious `BufferOverflowError` in plugins implementing `#format` https://github.com/fluent/fluentd/pull/5456
11
+ * in_syslog: enforce message_length_limit on TCP/TLS transport https://github.com/fluent/fluentd/pull/5502
12
+ * The default value of `message_length_limit` is changed from 2048 to 8192 to match rsyslog's default `MaxMessageSize`.
13
+ * output: fix incomplete path traversal check in extract_placeholders https://github.com/fluent/fluentd/pull/5501
14
+ * output: treat JSON::GeneratorError as unrecoverable error https://github.com/fluent/fluentd/pull/5423
15
+ * parser_syslog: fix NameError when RFC5424 timestamp has repeated spaces https://github.com/fluent/fluentd/pull/5500
16
+ * parser_syslog: fix NameError when RFC3164 timestamp has repeated spaces https://github.com/fluent/fluentd/pull/5497
17
+ * parser_syslog: avoid excessive backtracking when parsing malformed RFC5424 structured data https://github.com/fluent/fluentd/pull/5444
18
+ * chunk: ensure to close the Tempfile for decompressed data https://github.com/fluent/fluentd/pull/5486
19
+ * plugin base: bound the number of worker lock files by hashing the path into a fixed set of buckets https://github.com/fluent/fluentd/pull/5471
20
+ * out_forward: stop the endless "ack in response and chunk id in sent data are different" warning storm by discarding (instead of reusing) a keepalive socket whose ack failed or came back with a mismatched chunk id https://github.com/fluent/fluentd/pull/5445
21
+ * config: accept empty lines in quoted strings https://github.com/fluent/fluentd/pull/5478
22
+ * config: fix a config error when a single scalar value is given to an array option in YAML config syntax (for example `retryable_response_codes: 503`) https://github.com/fluent/fluentd/pull/5433
23
+ * supervisor: reduce memory usage of cleanup_lock_dir with huge number of lock files https://github.com/fluent/fluentd/pull/5472
24
+
25
+ ### Enhancement
26
+
27
+ * in_http: add `<auth>` for basic authentication and `<security>` for client network allowlisting https://github.com/fluent/fluentd/pull/5503
28
+
29
+ ### Misc
30
+
31
+ * gem: support json gem v3.x https://github.com/fluent/fluentd/pull/5493
32
+ * Add `allow_comments: true` option for json parser https://github.com/fluent/fluentd/pull/5432
33
+ * Set `allow_duplicate_key: true` for all `JSON.parse` call https://github.com/fluent/fluentd/pull/5430
34
+ * CI fixes
35
+ * https://github.com/fluent/fluentd/pull/5495
36
+ * https://github.com/fluent/fluentd/pull/5477
37
+ * https://github.com/fluent/fluentd/pull/5466
38
+ * https://github.com/fluent/fluentd/pull/5460
39
+ * https://github.com/fluent/fluentd/pull/5455
40
+ * https://github.com/fluent/fluentd/pull/5454
41
+ * https://github.com/fluent/fluentd/pull/5453
42
+ * https://github.com/fluent/fluentd/pull/5452
43
+ * https://github.com/fluent/fluentd/pull/5434
44
+ * https://github.com/fluent/fluentd/pull/5426
45
+ * https://github.com/fluent/fluentd/pull/5425
46
+ * https://github.com/fluent/fluentd/pull/5424
47
+
3
48
  ## Release v1.19.3 - 2026/06/25
4
49
 
5
50
  ### Bug Fix
@@ -326,7 +326,7 @@ case format
326
326
  when 'json'
327
327
  begin
328
328
  while line = $stdin.gets
329
- record = Yajl.load(line)
329
+ record = JSON.parse(line, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
330
330
  w.write(record)
331
331
  end
332
332
  rescue
@@ -28,7 +28,6 @@ module Fluent
28
28
  SPACING = /(?:[ \t\r\n]|\z|\#.*?(?:\z|[\r\n]))+/
29
29
  ZERO_OR_MORE_SPACING = /(?:[ \t\r\n]|\z|\#.*?(?:\z|[\r\n]))*/
30
30
  SPACING_WITHOUT_COMMENT = /(?:[ \t\r\n]|\z)+/
31
- LINE_END_WITHOUT_SPACING_AND_COMMENT = /(?:\z|[\r\n])/
32
31
 
33
32
  module ClassMethods
34
33
  def symbol(string)
@@ -21,11 +21,21 @@ require 'yajl'
21
21
  require 'socket'
22
22
  require 'ripper'
23
23
 
24
+ require 'fluent/env'
24
25
  require 'fluent/config/basic_parser'
25
26
 
26
27
  module Fluent
27
28
  module Config
28
29
  class LiteralParser < BasicParser
30
+ # A physical line break (LF, CR, or CRLF) inside a quoted string.
31
+ # CRLF is normalized to LF so that the same config text does not produce a
32
+ # different value depending on whether the file was saved with LF or CRLF.
33
+ # A lone CR is kept as-is, and an escaped "\r\n" still produces CRLF.
34
+ LINE_BREAK = /\r\n|[\r\n]/
35
+ # A backslash immediately followed by a physical line break.
36
+ # It works as a line continuation, so both are stripped from the value.
37
+ LINE_CONTINUATION = /\\#{LINE_BREAK}/o
38
+
29
39
  def self.unescape_char(c)
30
40
  case c
31
41
  when '"'
@@ -98,11 +108,10 @@ module Fluent
98
108
  else
99
109
  return string.join
100
110
  end
101
- elsif check(/[^"]#{LINE_END_WITHOUT_SPACING_AND_COMMENT}/o)
102
- if s = check(/[^\\]#{LINE_END_WITHOUT_SPACING_AND_COMMENT}/o)
103
- string << s
104
- end
105
- skip(/[^"]#{LINE_END_WITHOUT_SPACING_AND_COMMENT}/o)
111
+ elsif skip(LINE_CONTINUATION)
112
+ next
113
+ elsif s = scan(LINE_BREAK)
114
+ string << (s == "\r\n" ? "\n" : s)
106
115
  elsif s = scan(/\\./)
107
116
  string << eval_escape_char(s[1,1])
108
117
  elsif skip(/\#\{/)
@@ -125,6 +134,8 @@ module Fluent
125
134
  string << "'"
126
135
  elsif s = scan(/\\\\/)
127
136
  string << "\\"
137
+ elsif s = scan(LINE_BREAK)
138
+ string << (s == "\r\n" ? "\n" : s)
128
139
  elsif s = scan(/./)
129
140
  string << s
130
141
  else
@@ -241,7 +252,7 @@ EOM
241
252
  # '{"foo":"bar", #' -> '{"foo":"bar"}' (to check)
242
253
  parsed = nil
243
254
  begin
244
- parsed = JSON.parse(buffer + line_buffer.rstrip.sub(/,$/, '') + (is_array ? "]" : "}"))
255
+ parsed = JSON.parse(buffer + line_buffer.rstrip.sub(/,$/, '') + (is_array ? "]" : "}"), **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
245
256
  rescue JSON::ParserError
246
257
  # This '#' is in json string literals
247
258
  end
@@ -276,7 +287,7 @@ EOM
276
287
 
277
288
  line_buffer << char
278
289
  begin
279
- result = JSON.parse(buffer + line_buffer)
290
+ result = JSON.parse(buffer + line_buffer, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
280
291
  rescue JSON::ParserError
281
292
  # Incomplete json string yet
282
293
  end
@@ -201,7 +201,7 @@ module Fluent
201
201
  return nil if val.nil?
202
202
 
203
203
  param = if val.is_a?(String)
204
- val.start_with?('{') ? JSON.parse(val) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
204
+ val.start_with?('{') ? JSON.parse(val, **Fluent::DEFAULT_JSON_PARSE_OPTIONS) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
205
205
  else
206
206
  val
207
207
  end
@@ -228,7 +228,15 @@ module Fluent
228
228
  return nil if val.nil?
229
229
 
230
230
  param = if val.is_a?(String)
231
- val.start_with?('[') ? JSON.parse(val) : val.strip.split(/\s*,\s*/)
231
+ val.start_with?('[') ? JSON.parse(val, **Fluent::DEFAULT_JSON_PARSE_OPTIONS) : val.strip.split(/\s*,\s*/)
232
+ elsif val.is_a?(Array)
233
+ val
234
+ elsif val.is_a?(Numeric) || val == true || val == false
235
+ # Wrap only the bare scalars that Psych/YAML actually produces
236
+ # here (nil is handled by the early return above). Any other
237
+ # type (Hash, Symbol, Time, arbitrary objects) falls through and
238
+ # still raises "array required" below.
239
+ [val]
232
240
  else
233
241
  val
234
242
  end
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'fluent/env'
17
18
  require 'fluent/config/configure_proxy'
18
19
  require 'fluent/config/section'
19
20
  require 'fluent/config/error'
data/lib/fluent/daemon.rb CHANGED
@@ -5,9 +5,10 @@ here = File.dirname(__FILE__)
5
5
  $LOAD_PATH << File.expand_path(File.join(here, '..'))
6
6
 
7
7
  require 'serverengine'
8
+ require 'fluent/env'
8
9
  require 'fluent/supervisor'
9
10
 
10
11
  server_module = Fluent.const_get(ARGV[0])
11
12
  worker_module = Fluent.const_get(ARGV[1])
12
- params = JSON.parse(ARGV[2])
13
+ params = JSON.parse(ARGV[2], **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
13
14
  ServerEngine::Daemon.run_server(server_module, worker_module) { Fluent::Supervisor.serverengine_config(params) }
data/lib/fluent/env.rb CHANGED
@@ -26,6 +26,7 @@ module Fluent
26
26
  DEFAULT_SOCKET_PATH = ENV['FLUENT_SOCKET'] || '/var/run/fluent/fluent.sock'
27
27
  DEFAULT_BACKUP_DIR = ENV['FLUENT_BACKUP_DIR'] || '/tmp/fluent'
28
28
  DEFAULT_OJ_OPTIONS = Fluent::OjOptions.load_env
29
+ DEFAULT_JSON_PARSE_OPTIONS = { allow_duplicate_key: true, allow_comments: true }.freeze
29
30
  DEFAULT_DIR_PERMISSION = 0755
30
31
  DEFAULT_FILE_PERMISSION = 0644
31
32
  INSTANCE_ID = ENV['FLUENT_INSTANCE_ID'] || SecureRandom.uuid
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'zlib'
17
18
  require 'fluent/plugin'
18
19
  require 'fluent/configurable'
19
20
  require 'fluent/system_config'
@@ -69,9 +70,13 @@ module Fluent
69
70
  true
70
71
  end
71
72
 
73
+ LOCK_FILE_BUCKETS = 65536
74
+
72
75
  def get_lock_path(name)
73
- name = name.gsub(/[^a-zA-Z0-9]/, "_")
74
- File.join(@fluentd_lock_dir, "fluentd-#{name}.lock")
76
+ # The mapping from a name to a bucket MUST be identical across worker processes.
77
+ # Ruby's String#hash is randomly seeded per process and MUST NOT be used here.
78
+ bucket = Zlib.crc32(name.to_s) % LOCK_FILE_BUCKETS
79
+ File.join(@fluentd_lock_dir, "fluentd-bucket-#{bucket}.lock")
75
80
  end
76
81
 
77
82
  def acquire_worker_lock(name)
@@ -163,7 +163,7 @@ module Fluent
163
163
  end
164
164
 
165
165
  begin
166
- chunk = Fluent::Plugin::Buffer::FileChunk.new(m, path, mode, compress: @compress) # file chunk resumes contents of metadata
166
+ chunk = Fluent::Plugin::Buffer::FileChunk.new(m, path, mode, compress: @compress, decompression_size_limit: @decompression_size_limit) # file chunk resumes contents of metadata
167
167
  rescue Fluent::Plugin::Buffer::FileChunk::FileChunkError => e
168
168
  exist_broken_file = true
169
169
  handle_broken_files(path, mode, e)
@@ -220,9 +220,13 @@ module Fluent
220
220
  Tempfile.new('decompressed-data')
221
221
  end
222
222
  output_io.binmode if output_io.is_a?(Tempfile)
223
- decompress(input_io: chunk_io, output_io: output_io)
224
- output_io.seek(0, IO::SEEK_SET)
225
- yield output_io
223
+ begin
224
+ decompress(input_io: chunk_io, output_io: output_io)
225
+ output_io.seek(0, IO::SEEK_SET)
226
+ yield output_io
227
+ ensure
228
+ output_io.close! if output_io.is_a?(Tempfile)
229
+ end
226
230
  end
227
231
  end
228
232
  end
@@ -274,9 +278,13 @@ module Fluent
274
278
  Tempfile.new('decompressed-data')
275
279
  end
276
280
  output_io.binmode if output_io.is_a?(Tempfile)
277
- decompress(input_io: chunk_io, output_io: output_io, type: :zstd)
278
- output_io.seek(0, IO::SEEK_SET)
279
- yield output_io
281
+ begin
282
+ decompress(input_io: chunk_io, output_io: output_io, type: :zstd)
283
+ output_io.seek(0, IO::SEEK_SET)
284
+ yield output_io
285
+ ensure
286
+ output_io.close! if output_io.is_a?(Tempfile)
287
+ end
280
288
  end
281
289
  end
282
290
  end
@@ -168,9 +168,11 @@ module Fluent
168
168
 
169
169
  def open(**kwargs, &block)
170
170
  @chunk.seek(0, IO::SEEK_SET)
171
- val = yield @chunk
172
- @chunk.seek(0, IO::SEEK_END) if self.staged?
173
- val
171
+ begin
172
+ yield @chunk
173
+ ensure
174
+ @chunk.seek(0, IO::SEEK_END) if self.staged?
175
+ end
174
176
  end
175
177
 
176
178
  def self.assume_chunk_state(path)
@@ -141,9 +141,11 @@ module Fluent
141
141
 
142
142
  def open(**kwargs, &block)
143
143
  @chunk.seek(0, IO::SEEK_SET)
144
- val = yield @chunk
145
- @chunk.seek(0, IO::SEEK_END) if self.staged?
146
- val
144
+ begin
145
+ yield @chunk
146
+ ensure
147
+ @chunk.seek(0, IO::SEEK_END) if self.staged?
148
+ end
147
149
  end
148
150
 
149
151
  def self.assume_chunk_state(path)
@@ -601,13 +601,19 @@ module Fluent
601
601
  metadata = chunk.metadata
602
602
  log.on_trace { log.trace "purging a chunk", instance: self.object_id, chunk_id: dump_unique_id_hex(chunk_id), metadata: metadata }
603
603
 
604
+ bytesize = chunk.bytesize
604
605
  begin
605
- bytesize = chunk.bytesize
606
606
  chunk.purge
607
- @queue_size_metrics.sub(bytesize)
608
607
  rescue => e
609
608
  log.error "failed to purge buffer chunk", chunk_id: dump_unique_id_hex(chunk_id), error_class: e.class, error: e
610
609
  log.error_backtrace
610
+ ensure
611
+ # Always release the queued byte counter, even when purge raises
612
+ # (e.g. unlink/close failure on the buffer path). Otherwise
613
+ # @queue_size is never decremented for a dequeued chunk that is
614
+ # gone from both @queue and @dequeued, so the leak ratchets toward
615
+ # total_limit_size and storable? becomes permanently false (#5468).
616
+ @queue_size_metrics.sub(bytesize)
611
617
  end
612
618
 
613
619
  @dequeued_num[chunk.metadata] -= 1
@@ -850,7 +856,12 @@ module Fluent
850
856
  # As already processed content is kept after rollback, then unstaged chunk should be queued.
851
857
  # After that, re-process current split again.
852
858
  # New chunk should be allocated, to do it, modify @stage and so on.
853
- synchronize { @stage.delete(modified_metadata) }
859
+ synchronize do
860
+ @stage.delete(modified_metadata)
861
+ # Subtract this chunk's already-counted staged bytes here;
862
+ # otherwise @stage_size leaks upward over the buffer's lifetime.
863
+ @stage_size_metrics.sub(original_bytesize) if chunk.staged?
864
+ end
854
865
  staged_chunk_used = false
855
866
  chunk.unstaged!
856
867
  break
@@ -912,8 +923,15 @@ module Fluent
912
923
  ]
913
924
 
914
925
  def statistics
915
- stage_size, queue_size = @stage_size_metrics.get, @queue_size_metrics.get
916
- buffer_space = 1.0 - ((stage_size + queue_size * 1.0) / @total_limit_size)
926
+ # Export-only clamp: internal gauges may go transiently negative during
927
+ # the deferred stage_size add vs enqueue_chunk sub race (#5303, #2712).
928
+ # Clamping the gauge store itself would turn that into a permanent
929
+ # over-count and break Buffer#storable? -- keep raw gauge semantics.
930
+ stage_size = [@stage_size_metrics.get, 0].max
931
+ queue_size = [@queue_size_metrics.get, 0].max
932
+ denom = @total_limit_size.to_f
933
+ # denom > 0 already excludes 0/0 NaN; stage/queue are floored above.
934
+ buffer_space = denom > 0.0 ? (1.0 - (stage_size + queue_size).to_f / denom).clamp(0.0, 1.0) : 0.0
917
935
  @stage_length_metrics.set(@stage.size)
918
936
  @queue_length_metrics.set(@queue.size)
919
937
  @available_buffer_space_ratios_metrics.set(buffer_space * 100)
@@ -75,9 +75,9 @@ module Fluent
75
75
 
76
76
  def io_decompress(input, output, type = :gzip)
77
77
  if type == :gzip
78
- Extractor.io_decompress_gzip(input, output)
78
+ Extractor.io_decompress_gzip(input, output, limit: @decompression_size_limit || DEFAULT_DECOMPRESSION_SIZE_LIMIT)
79
79
  elsif type == :zstd
80
- Extractor.io_decompress_zstd(input, output)
80
+ Extractor.io_decompress_zstd(input, output, limit: @decompression_size_limit || DEFAULT_DECOMPRESSION_SIZE_LIMIT)
81
81
  else
82
82
  raise ArgumentError, "Unknown compression type: #{type}"
83
83
  end
@@ -26,6 +26,9 @@ module Fluent
26
26
 
27
27
  BYTES_TO_READ = 64 * 1024
28
28
  INFLATE_BYTES_TO_READ = 1024
29
+ # Zstd::StreamReader#read takes a number of compressed bytes, unlike
30
+ # Zlib::GzipReader#read which takes decompressed bytes
31
+ ZSTD_BYTES_TO_READ = 1024
29
32
 
30
33
  def self.decompress_gzip(compressed_data, limit:)
31
34
  io = StringIO.new(compressed_data)
@@ -55,8 +58,7 @@ module Fluent
55
58
  reader = Zstd::StreamReader.new(io)
56
59
  out = ''
57
60
  loop do
58
- # Zstd::StreamReader needs to specify the size of the buffer
59
- out << reader.read(BYTES_TO_READ)
61
+ out << reader.read(ZSTD_BYTES_TO_READ)
60
62
  if out.bytesize > limit
61
63
  raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
62
64
  end
@@ -88,10 +90,15 @@ module Fluent
88
90
  out
89
91
  end
90
92
 
91
- def self.io_decompress_gzip(input, output)
93
+ def self.io_decompress_gzip(input, output, limit:)
94
+ size = 0
92
95
  loop do
93
96
  reader = Zlib::GzipReader.new(input)
94
97
  while (chunk = reader.read(BYTES_TO_READ))
98
+ size += chunk.bytesize
99
+ if size > limit
100
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
101
+ end
95
102
  output.write(chunk)
96
103
  end
97
104
  unused = reader.unused
@@ -105,11 +112,15 @@ module Fluent
105
112
  output
106
113
  end
107
114
 
108
- def self.io_decompress_zstd(input, output)
115
+ def self.io_decompress_zstd(input, output, limit:)
109
116
  reader = Zstd::StreamReader.new(input)
117
+ size = 0
110
118
  loop do
111
- # Zstd::StreamReader needs to specify the size of the buffer
112
- chunk = reader.read(BYTES_TO_READ)
119
+ chunk = reader.read(ZSTD_BYTES_TO_READ)
120
+ size += chunk.bytesize
121
+ if size > limit
122
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
123
+ end
113
124
  output.write(chunk)
114
125
  # Zstd::StreamReader doesn't provide unused data, so we have to manually adjust the position
115
126
  break if input.eof?
@@ -18,6 +18,7 @@ require 'socket'
18
18
  require 'json'
19
19
  require 'ostruct'
20
20
 
21
+ require 'fluent/env'
21
22
  require 'fluent/plugin/filter'
22
23
  require 'fluent/config/error'
23
24
  require 'fluent/event'
@@ -116,7 +117,7 @@ module Fluent::Plugin
116
117
 
117
118
  def parse_value(value_str)
118
119
  if value_str.start_with?('{', '[')
119
- JSON.parse(value_str)
120
+ JSON.parse(value_str, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
120
121
  else
121
122
  value_str
122
123
  end
@@ -24,6 +24,8 @@ require 'webrick/httputils'
24
24
  require 'uri'
25
25
  require 'socket'
26
26
  require 'json'
27
+ require 'ipaddr'
28
+ require 'openssl'
27
29
 
28
30
  module Fluent::Plugin
29
31
  class InHttpParser < Parser
@@ -90,6 +92,22 @@ module Fluent::Plugin
90
92
  desc "Add prefix to incoming tag"
91
93
  config_param :add_tag_prefix, :string, default: nil
92
94
 
95
+ config_section :auth, required: false, multi: false do
96
+ desc 'The method for HTTP authentication'
97
+ config_param :method, :enum, list: [:basic], default: :basic
98
+ desc 'The username for basic authentication'
99
+ config_param :username, :string
100
+ desc 'The password for basic authentication'
101
+ config_param :password, :string, secret: true
102
+ end
103
+
104
+ config_section :security, required: false, multi: false do
105
+ config_section :client, param_name: :clients, required: false, multi: true do
106
+ desc 'Address or network of the client, IPv4 or IPv6. e.g. 192.168.1.10, 192.168.0.0/16, fd00::/8'
107
+ config_param :network, :string
108
+ end
109
+ end
110
+
93
111
  config_section :parse do
94
112
  config_set_default :@type, 'in_http'
95
113
  end
@@ -100,6 +118,7 @@ module Fluent::Plugin
100
118
  super
101
119
 
102
120
  @km = nil
121
+ @nodes = []
103
122
  @format_name = nil
104
123
  @parser_time_key = nil
105
124
 
@@ -127,6 +146,8 @@ module Fluent::Plugin
127
146
 
128
147
  raise Fluent::ConfigError, "'add_tag_prefix' parameter must not be empty" if @add_tag_prefix && @add_tag_prefix.empty?
129
148
 
149
+ configure_security if @security
150
+
130
151
  m = if @parser_configs.first['@type'] == 'in_http'
131
152
  @parser_msgpack = parser_create(usage: 'parser_in_http_msgpack', type: 'msgpack')
132
153
  @parser_msgpack.time_key = nil
@@ -260,11 +281,33 @@ module Fluent::Plugin
260
281
 
261
282
  private
262
283
 
284
+ def configure_security
285
+ @nodes = @security.clients.map do |client|
286
+ begin
287
+ IPAddr.new(client.network)
288
+ rescue ArgumentError
289
+ raise Fluent::ConfigError, "network '#{client.network}' address format is invalid"
290
+ end
291
+ end
292
+ end
293
+
294
+ def allowed_client?(address)
295
+ return true if @nodes.empty?
296
+ @nodes.any? { |node| node.include?(address) rescue false }
297
+ end
298
+
263
299
  def on_server_connect(conn)
300
+ unless allowed_client?(conn.remote_addr)
301
+ log.warn "client address does not match any allowed network", address: conn.remote_addr
302
+ conn.data {|_data| }
303
+ conn.close
304
+ return
305
+ end
306
+
264
307
  handler = Handler.new(conn, @km, method(:on_request),
265
308
  @body_size_limit, @decompression_size_limit, @format_name, log,
266
309
  @cors_allow_origins, @cors_allow_credentials,
267
- @add_query_params)
310
+ @add_query_params, @auth)
268
311
 
269
312
  conn.on(:data) do |data|
270
313
  handler.on_read(data)
@@ -347,7 +390,7 @@ module Fluent::Plugin
347
390
  attr_reader :content_type
348
391
 
349
392
  def initialize(io, km, callback, body_size_limit, decompression_size_limit, format_name, log,
350
- cors_allow_origins, cors_allow_credentials, add_query_params)
393
+ cors_allow_origins, cors_allow_credentials, add_query_params, auth = nil)
351
394
  @io = io
352
395
  @km = km
353
396
  @callback = callback
@@ -360,6 +403,8 @@ module Fluent::Plugin
360
403
  @cors_allow_credentials = cors_allow_credentials
361
404
  @idle = 0
362
405
  @add_query_params = add_query_params
406
+ @auth = auth
407
+ @authorization = nil
363
408
  @km.add(self)
364
409
 
365
410
  @remote_port, @remote_addr = io.remote_port, io.remote_addr
@@ -399,6 +444,7 @@ module Fluent::Plugin
399
444
  @env = {}
400
445
  @content_type = ""
401
446
  @content_encoding = ""
447
+ @authorization = nil
402
448
  headers.each_pair {|k,v|
403
449
  @env["HTTP_#{k.tr('-','_').upcase}"] = v
404
450
  case k
@@ -426,8 +472,22 @@ module Fluent::Plugin
426
472
  @access_control_request_method = v
427
473
  when /\AAccess-Control-Request-Headers\Z/i
428
474
  @access_control_request_headers = v
475
+ when /\AAuthorization\Z/i
476
+ @authorization = v.is_a?(Array) ? v.first : v
429
477
  end
430
478
  }
479
+
480
+ if @auth
481
+ # Never let the credential reach a record built by add_http_headers.
482
+ @env.delete("HTTP_AUTHORIZATION")
483
+
484
+ unless preflight_request? || authenticate
485
+ @log.warn "authentication failed", address: @remote_addr
486
+ send_response_and_close(RES_401_STATUS, {'WWW-Authenticate' => AUTH_CHALLENGE}, "")
487
+ return
488
+ end
489
+ end
490
+
431
491
  if expect
432
492
  if expect == '100-continue'.freeze
433
493
  if !size || size < @body_size_limit
@@ -442,6 +502,8 @@ module Fluent::Plugin
442
502
  end
443
503
 
444
504
  def on_body(chunk)
505
+ return if closing?
506
+
445
507
  if @body.bytesize + chunk.bytesize > @body_size_limit
446
508
  unless closing?
447
509
  send_response_and_close("413 Request Entity Too Large", {}, "Too large")
@@ -452,6 +514,8 @@ module Fluent::Plugin
452
514
  end
453
515
 
454
516
  RES_200_STATUS = "200 OK".freeze
517
+ RES_401_STATUS = "401 Unauthorized".freeze
518
+ AUTH_CHALLENGE = 'Basic realm="fluentd"'.freeze
455
519
  RES_403_STATUS = "403 Forbidden".freeze
456
520
 
457
521
  # Azure App Service sends GET requests for health checking purpose.
@@ -651,6 +715,23 @@ module Fluent::Plugin
651
715
  def parse_query(query)
652
716
  query.nil? ? {} : Hash[URI.decode_www_form(query, Encoding::ASCII_8BIT)]
653
717
  end
718
+
719
+ # Browsers never attach Authorization to a CORS preflight.
720
+ def preflight_request?
721
+ @parser.http_method == 'OPTIONS'.freeze
722
+ end
723
+
724
+ def authenticate
725
+ scheme, credentials = @authorization.to_s.split(' ', 2)
726
+ return false unless scheme == 'Basic'.freeze
727
+
728
+ username, password = credentials.to_s.unpack1('m').to_s.split(':', 2)
729
+ username_matched = OpenSSL.secure_compare(username.to_s, @auth.username)
730
+ password_matched = OpenSSL.secure_compare(password.to_s, @auth.password)
731
+ username_matched && password_matched
732
+ rescue ArgumentError
733
+ false
734
+ end
654
735
  end
655
736
  end
656
737
  end
@@ -16,6 +16,7 @@
16
16
 
17
17
  require 'json'
18
18
 
19
+ require 'fluent/env'
19
20
  require 'fluent/config/types'
20
21
  require 'fluent/plugin/input'
21
22
  require 'fluent/plugin/output'
@@ -101,7 +102,7 @@ module Fluent::Plugin
101
102
  end
102
103
 
103
104
  def render_ltsv(obj, code: 200)
104
- normalized = JSON.parse(obj.to_json)
105
+ normalized = JSON.parse(obj.to_json, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
105
106
  text = ''
106
107
  normalized.each do |hash|
107
108
  row = []
@@ -16,6 +16,7 @@
16
16
 
17
17
  require 'json'
18
18
 
19
+ require 'fluent/env'
19
20
  require 'fluent/plugin/input'
20
21
  require 'fluent/config/error'
21
22
 
@@ -44,7 +45,7 @@ module Fluent::Plugin
44
45
  desc "The sample data to be generated. An array of JSON hashes or a single JSON hash."
45
46
  config_param :sample, alias: :dummy, default: [{"message" => "sample"}] do |val|
46
47
  begin
47
- parsed = JSON.parse(val)
48
+ parsed = JSON.parse(val, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
48
49
  rescue JSON::ParserError => ex
49
50
  # Fluent::ConfigParseError, "got incomplete JSON" will be raised
50
51
  # at literal_parser.rb with --use-v1-config, but I had to
@@ -98,8 +98,8 @@ module Fluent::Plugin
98
98
  desc 'The field name of the facility.'
99
99
  config_param :facility_key, :string, default: nil
100
100
 
101
- desc "The max bytes of message"
102
- config_param :message_length_limit, :size, default: 2048
101
+ desc "The max bytes of message. The messages exceeding this limit are dropped (UDP, TCP/TLS with traditional frame_type) or the connection is closed (TCP/TLS with octet_count frame_type)."
102
+ config_param :message_length_limit, :size, default: 8192
103
103
 
104
104
  config_param :blocking_timeout, :time, default: 0.5
105
105
 
@@ -190,29 +190,95 @@ module Fluent::Plugin
190
190
  resolve_name: @resolve_hostname,
191
191
  send_keepalive_packet: @send_keepalive_packet
192
192
  ) do |conn|
193
+ discard_till_next_delimiter = false
193
194
  conn.data do |data|
194
195
  buffer = conn.buffer
195
196
  buffer << data
196
197
  pos = 0
197
198
  if octet_count_frame
199
+ # Once an octet-counted stream contains an untrustworthy frame header, the stream
200
+ # can't be resynchronized. So close the connection instead of skipping bytes.
201
+ close_connection = false
198
202
  while idx = buffer.index(delimiter, pos)
199
- num = Integer(buffer[pos..idx])
203
+ begin
204
+ num = Integer(buffer[pos..idx])
205
+ rescue ArgumentError
206
+ log.info "The message length is not a number, closing connection"
207
+ log.on_debug { log.debug "The message length is not a number:", head: buffer[pos, 32].dump }
208
+ close_connection = true
209
+ break
210
+ end
211
+
212
+ if num < 0
213
+ log.info "The message length is negative, closing connection:", size: num
214
+ close_connection = true
215
+ break
216
+ end
217
+
218
+ if num > @message_length_limit
219
+ log.info "The message length exceeds 'message_length_limit', closing connection:", limit: @message_length_limit, size: num
220
+ close_connection = true
221
+ break
222
+ end
223
+
200
224
  msg = buffer[idx + delimiter_size, num]
201
225
  if msg.size != num
226
+ # The frame is not fully received yet.
202
227
  break
203
228
  end
204
229
 
205
230
  pos = idx + delimiter_size + num
206
231
  message_handler(msg, conn)
207
232
  end
233
+ buffer.slice!(0, pos) if pos > 0
234
+
235
+ # If the remaining buffer has no delimiter, no message length can be parsed from it.
236
+ # Since a valid frame header is much shorter than the limit, such a buffer never
237
+ # becomes a valid frame. (Note that a partially received valid frame always has a
238
+ # delimiter after its length, so this doesn't drop it even if it slightly exceeds
239
+ # the limit with its length header.)
240
+ if !close_connection && !buffer.index(delimiter) && buffer.bytesize > @message_length_limit
241
+ log.info "The buffer size exceeds 'message_length_limit', closing connection:", limit: @message_length_limit, size: buffer.bytesize
242
+ log.on_debug { log.debug "The buffer size exceeds 'message_length_limit':", head: buffer[...32] }
243
+ close_connection = true
244
+ end
245
+
246
+ if close_connection
247
+ buffer.clear
248
+ conn.close
249
+ next
250
+ end
208
251
  else
209
252
  while idx = buffer.index(delimiter, pos)
210
253
  msg = buffer[pos...idx]
211
254
  pos = idx + delimiter_size
255
+
256
+ if discard_till_next_delimiter
257
+ discard_till_next_delimiter = false
258
+ next
259
+ end
260
+
261
+ if msg.bytesize > @message_length_limit
262
+ log.info "The received data is larger than 'message_length_limit', dropped:", limit: @message_length_limit, size: msg.bytesize
263
+ log.on_debug { log.debug "The received data is larger than 'message_length_limit':", head: msg[...32] }
264
+ next
265
+ end
266
+
212
267
  message_handler(msg, conn)
213
268
  end
269
+ buffer.slice!(0, pos) if pos > 0
270
+
271
+ # If the buffer size exceeds the limit here, it means that the next message will
272
+ # definitely exceed the limit. So we should clear the buffer here. Otherwise, it will
273
+ # keep storing useless data until the next delimiter comes.
274
+ if buffer.bytesize > @message_length_limit
275
+ log.info "The buffer size exceeds 'message_length_limit', cleared:", limit: @message_length_limit, size: buffer.bytesize
276
+ log.on_debug { log.debug "The buffer size exceeds 'message_length_limit':", head: buffer[...32] }
277
+ buffer.clear
278
+ # We should discard the subsequent data until the next delimiter comes.
279
+ discard_till_next_delimiter = true
280
+ end
214
281
  end
215
- buffer.slice!(0, pos) if pos > 0
216
282
  end
217
283
  end
218
284
  end
@@ -78,6 +78,24 @@ module Fluent::Plugin
78
78
  end
79
79
  end
80
80
 
81
+ # Drop a socket instead of returning it to the keepalive reuse pool.
82
+ #
83
+ # Used when a chunk's ack timed out or came back with a mismatched chunk
84
+ # id: such a connection may still carry an in-flight/stale ack for a
85
+ # previous chunk. Reusing it would let that stale ack be read against a
86
+ # chunk sent on the reused socket, permanently offsetting every following
87
+ # ack and flooding the log with 'ack in response and chunk id in sent data
88
+ # are different' warnings (see #4057). Revoking removes it from the pool so
89
+ # the next chunk uses a fresh connection with a clean ack stream.
90
+ def discard(sock)
91
+ if @socket_cache
92
+ @socket_cache.revoke(sock)
93
+ else
94
+ sock.close_write rescue nil
95
+ sock.close rescue nil
96
+ end
97
+ end
98
+
81
99
  private
82
100
 
83
101
  def connect_keepalive(host:, port:, hostname:, ack: nil)
@@ -525,17 +525,28 @@ module Fluent::Plugin
525
525
 
526
526
  def ack_check(select_interval)
527
527
  @ack_handler.collect_response(select_interval) do |chunk_id, node, sock, result|
528
- @connection_manager.close(sock)
529
-
530
528
  case result
531
529
  when AckHandler::Result::SUCCESS
530
+ @connection_manager.close(sock)
532
531
  commit_write(chunk_id)
533
532
  when AckHandler::Result::FAILED
533
+ # The ack timed out or the peer closed the connection. A keepalive
534
+ # socket may still have an in-flight ack for this chunk, so drop it
535
+ # instead of returning it to the reuse pool; otherwise a late ack
536
+ # could later be read against a chunk sent on the reused socket (#4057).
537
+ @connection_manager.discard(sock)
534
538
  node&.disable!
535
539
  rollback_write(chunk_id, update_retry: false) if chunk_id
536
540
  when AckHandler::Result::CHUNKID_UNMATCHED
541
+ # The ack read from this socket belongs to a different chunk than the
542
+ # one tracked for it: the keepalive connection's ack stream is out of
543
+ # sync (e.g. a stale ack left over from before an aggregator restart).
544
+ # Discard the socket so the following acks are not each offset by one,
545
+ # which would otherwise flood the log with mismatch warnings (#4057).
546
+ @connection_manager.discard(sock)
537
547
  rollback_write(chunk_id, update_retry: false)
538
548
  else
549
+ @connection_manager.close(sock)
539
550
  log.warn("BUG: invalid status #{result} #{chunk_id}")
540
551
 
541
552
  if chunk_id
@@ -28,6 +28,7 @@ require 'fluent/unique_id'
28
28
  require 'fluent/clock'
29
29
  require 'fluent/ext_monitor_require'
30
30
 
31
+ require 'json'
31
32
  require 'time'
32
33
 
33
34
  module Fluent
@@ -44,8 +45,21 @@ module Fluent
44
45
  CHUNK_KEY_PLACEHOLDER_PATTERN = /\$\{([-_.@$a-zA-Z0-9]+)\}/
45
46
  CHUNK_TAG_PLACEHOLDER_PATTERN = /\$\{(tag(?:\[-?\d+\])?)\}/
46
47
  CHUNK_ID_PLACEHOLDER_PATTERN = /\$\{chunk_id\}/
47
- INVALID_PATH_COMPONENT_PATTERN = %r{\.\.[/\\]|^[/\\]}
48
- PARENT_DIRECTORY_PATTERN = %r{\.\.[/\\]}
48
+
49
+ # Matches absolute paths, including the POSIX root, Windows drive roots, and UNC paths (e.g., \\host\share).
50
+ # It also accounts for forward slashes used as directory separators in Windows.
51
+ ABSOLUTE_PATH_PATTERN = %r{\A[/\\]}
52
+ # Matches Windows drive specifications.
53
+ # A trailing separator after the colon is not required, because a drive-relative path like C:Windows can still escape a relative path template.
54
+ WINDOWS_DRIVE_PATTERN = %r{\A[a-zA-Z]:}
55
+ # Matches .. only when it acts as an independent path component (e.g., ../etc or foo/..),
56
+ # ignoring dots embedded within strings like app..web or a..b/c.
57
+ # It uses zero-width negative lookarounds to avoid consuming characters.
58
+ # This ensures that adjacent components like /a/../../b are accurately detected and counted without missing any.
59
+ PARENT_DIRECTORY_PATTERN = %r{(?<![^/\\])\.\.(?![^/\\])}
60
+
61
+ INVALID_PATH_COMPONENT_PATTERN =
62
+ Regexp.union(ABSOLUTE_PATH_PATTERN, WINDOWS_DRIVE_PATTERN, PARENT_DIRECTORY_PATTERN)
49
63
 
50
64
  CHUNKING_FIELD_WARN_NUM = 4
51
65
 
@@ -878,12 +892,13 @@ module Fluent
878
892
 
879
893
  replace
880
894
  end
881
- # Check if the number of parent directory components (../) has increased due to variable substitution
882
- if rvalue.match?(PARENT_DIRECTORY_PATTERN)
883
- if rvalue.scan(PARENT_DIRECTORY_PATTERN).size > str.scan(PARENT_DIRECTORY_PATTERN).size
884
- raise Fluent::UnrecoverableError, "Invalid path component detected, replaced to: #{rvalue}"
885
- end
886
- end
895
+ end
896
+
897
+ # Verifies that the number of parent directory traversals (..) has not increased after variable substitution.
898
+ # While each replaced value is validated individually, this step prevents harmless values from concatenating into a dangerous path (e.g., ${tag}${key}/ becoming ../ if both tag and key are .).
899
+ if rvalue.match?(PARENT_DIRECTORY_PATTERN) &&
900
+ rvalue.scan(PARENT_DIRECTORY_PATTERN).size > str.scan(PARENT_DIRECTORY_PATTERN).size
901
+ raise Fluent::UnrecoverableError, "Invalid path component detected, replaced to: #{rvalue}"
887
902
  end
888
903
 
889
904
  if rvalue =~ CHUNK_KEY_PLACEHOLDER_PATTERN
@@ -1212,7 +1227,7 @@ module Fluent
1212
1227
  end
1213
1228
  end
1214
1229
 
1215
- UNRECOVERABLE_ERRORS = [Fluent::UnrecoverableError, TypeError, ArgumentError, NoMethodError, MessagePack::UnpackError, EncodingError]
1230
+ UNRECOVERABLE_ERRORS = [Fluent::UnrecoverableError, TypeError, ArgumentError, NoMethodError, MessagePack::UnpackError, EncodingError, JSON::GeneratorError]
1216
1231
 
1217
1232
  def try_flush
1218
1233
  chunk = @buffer.dequeue_chunk
@@ -1319,9 +1334,14 @@ module Fluent
1319
1334
  if @buffer.disable_chunk_backup
1320
1335
  log.warn "disable_chunk_backup is true. #{dump_unique_id_hex(chunk.unique_id)} chunk is thrown away"
1321
1336
  else
1322
- @buffer.backup(chunk.unique_id) { |f|
1323
- chunk.write_to(f)
1324
- }
1337
+ begin
1338
+ @buffer.backup(chunk.unique_id) { |f|
1339
+ chunk.write_to(f)
1340
+ }
1341
+ rescue => e
1342
+ log.warn "failed to back up #{dump_unique_id_hex(chunk.unique_id)} chunk. it is thrown away", error: e
1343
+ log.warn_backtrace
1344
+ end
1325
1345
  end
1326
1346
  commit_write(chunk.unique_id, secondary: using_secondary, delayed: delayed_commit)
1327
1347
  end
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'fluent/env'
17
18
  require 'fluent/plugin/parser'
18
19
  require 'fluent/time'
19
20
  require 'fluent/oj_options'
@@ -38,6 +39,10 @@ module Fluent
38
39
 
39
40
  config_set_default :time_type, :float
40
41
 
42
+ # Use a shared proc rather than a per-call lambda so that
43
+ # configure_json_parser returns the same object every time.
44
+ JSON_PARSE_PROC = ->(text) { JSON.parse(text, **Fluent::DEFAULT_JSON_PARSE_OPTIONS) }
45
+
41
46
  def configure(conf)
42
47
  if conf.has_key?('time_format')
43
48
  conf['time_type'] ||= 'string'
@@ -54,7 +59,7 @@ module Fluent
54
59
 
55
60
  log&.info "Oj is not installed, and failing back to JSON for json parser"
56
61
  configure_json_parser(:json)
57
- when :json then [JSON.method(:parse), JSON::ParserError]
62
+ when :json then [JSON_PARSE_PROC, JSON::ParserError]
58
63
  when :yajl then [Yajl.method(:load), Yajl::ParseError]
59
64
  else
60
65
  raise "BUG: unknown json parser specified: #{name}"
@@ -44,7 +44,7 @@ module Fluent
44
44
  RFC3164_CAPTURES = RFC3164_WITHOUT_TIME_AND_PRI_REGEXP.names.freeze
45
45
  RFC3164_PRI_REGEXP = /^<(?<pri>[0-9]{1,3})>/
46
46
 
47
- RFC5424_WITHOUT_TIME_AND_PRI_REGEXP = /(?<host>[!-~]{1,255}) (?<ident>[!-~]{1,48}) (?<pid>[!-~]{1,128}) (?<msgid>[!-~]{1,32}) (?<extradata>(?:\-|(?:\[.*?(?<!\\)\])+))(?: (?<message>.+))?\z/m
47
+ RFC5424_WITHOUT_TIME_AND_PRI_REGEXP = /(?<host>[!-~]{1,255}) (?<ident>[!-~]{1,48}) (?<pid>[!-~]{1,128}) (?<msgid>[!-~]{1,32}) (?<extradata>(?:\-|(?:\[.*?(?<!\\)\])))(?: (?<message>.+))?\z/m
48
48
  RFC5424_CAPTURES = RFC5424_WITHOUT_TIME_AND_PRI_REGEXP.names.freeze
49
49
  RFC5424_PRI_REGEXP = /^<(?<pri>\d{1,3})>\d\d{0,2}\s/
50
50
 
@@ -314,7 +314,7 @@ module Fluent
314
314
  i = text.index(SPLIT_CHAR, i + 1)
315
315
  end
316
316
 
317
- time_str = sq ? text.slice(idx, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
317
+ time_str = sq ? text.slice(cursor, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
318
318
  cursor = i + 1
319
319
  end
320
320
 
@@ -406,7 +406,7 @@ module Fluent
406
406
  i = text.index(SPLIT_CHAR, i + 1)
407
407
  end
408
408
 
409
- time_str = sq ? text.slice(idx, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
409
+ time_str = sq ? text.slice(cursor, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
410
410
  cursor = i + 1
411
411
  end
412
412
 
@@ -16,6 +16,7 @@
16
16
 
17
17
  require 'cool.io'
18
18
 
19
+ require 'fluent/env'
19
20
  require 'fluent/plugin_helper'
20
21
  require 'fluent/plugin/service_discovery'
21
22
 
@@ -75,7 +76,7 @@ module Fluent
75
76
  -> (v) { YAML.safe_load(v).map }
76
77
  when :json
77
78
  require 'json'
78
- -> (v) { JSON.parse(v) }
79
+ -> (v) { JSON.parse(v, **Fluent::DEFAULT_JSON_PARSE_OPTIONS) }
79
80
  end
80
81
  end
81
82
 
@@ -90,7 +90,7 @@ module Fluent
90
90
  log.warn "detect empty plugin storage file during startup. Ignored: #{@path}"
91
91
  return
92
92
  end
93
- data = JSON.parse(data)
93
+ data = JSON.parse(data, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
94
94
  raise Fluent::ConfigError, "Invalid contents (not object) in plugin storage file: '#{@path}'" unless data.is_a?(Hash)
95
95
  rescue => e
96
96
  log.error "failed to read data from plugin storage file", path: @path, error: e
@@ -114,7 +114,7 @@ module Fluent
114
114
  return unless File.exist?(@path)
115
115
  begin
116
116
  json_string = File.open(@path, 'r:utf-8:utf-8'){ |io| io.read }
117
- json = JSON.parse(json_string)
117
+ json = JSON.parse(json_string, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
118
118
  unless json.is_a?(Hash)
119
119
  log.error "broken content for plugin storage (Hash required: ignored)", type: json.class
120
120
  log.debug "broken content", content: json_string
@@ -100,7 +100,14 @@ module Fluent
100
100
  end
101
101
 
102
102
  def cleanup_lock_dir
103
- FileUtils.rm(Dir.glob(File.join(@fluentd_lock_dir, "fluentd-*.lock")))
103
+ begin
104
+ Dir.each_child(@fluentd_lock_dir) do |name|
105
+ FileUtils.rm_f(File.join(@fluentd_lock_dir, name)) if File.fnmatch?("fluentd-*.lock", name)
106
+ end
107
+ rescue Errno::ENOENT
108
+ # Directory is already missing. Fall through and let rmdir below
109
+ # fail in the same way as the current implementation.
110
+ end
104
111
  FileUtils.rmdir(@fluentd_lock_dir)
105
112
  end
106
113
 
@@ -16,6 +16,6 @@
16
16
 
17
17
  module Fluent
18
18
 
19
- VERSION = '1.19.3'
19
+ VERSION = '1.19.4'
20
20
 
21
21
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluentd
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.3
4
+ version: 1.19.4
5
5
  platform: x64-mingw32
6
6
  authors:
7
7
  - Sadayuki Furuhashi
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-06-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -954,7 +953,6 @@ metadata:
954
953
  source_code_uri: https://github.com/fluent/fluentd
955
954
  changelog_uri: https://github.com/fluent/fluentd/blob/master/CHANGELOG.md
956
955
  bug_tracker_uri: https://github.com/fluent/fluentd/issues
957
- post_install_message:
958
956
  rdoc_options: []
959
957
  require_paths:
960
958
  - lib
@@ -969,8 +967,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
969
967
  - !ruby/object:Gem::Version
970
968
  version: '0'
971
969
  requirements: []
972
- rubygems_version: 3.5.22
973
- signing_key:
970
+ rubygems_version: 4.0.20
974
971
  specification_version: 4
975
972
  summary: Fluentd event collector
976
973
  test_files: []