Skip to content

Fix crash when receive a BigDecimal inside _@timestamp - #67

Merged
andsel merged 15 commits into
logstash-plugins:mainfrom
wpjunior:fix/crash-by-bigdecimal
Dec 21, 2021
Merged

andsel merged 15 commits into
logstash-plugins:mainfrom
wpjunior:fix/crash-by-bigdecimal

Conversation

@wpjunior

@wpjunior wpjunior commented Mar 30, 2021 •

Copy link
Copy Markdown
Contributor

The UDP server crashes when receives a GELF message like that {"full_message": "my message", "_@timestamp": 12345678889}

I wrote a test case to show that behavior.

Thanks


Release notes

Safe parsing of _@timestamp to avoid crashing the plugin and Logstash

What does this PR do?

This PR safely cast the values of _@timestamp field in GELF and in case of error skip the event, avoiding Logstash crash

Why is it important/What is the impact to the user?

Permit to handle correctly the numerical values in _@timestamp without stopping Logstash process in case of error.

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have made corresponding change to the default configuration files (and/or docker env variables)
  • I have added tests that prove my fix is effective or that my feature works

Author's Checklist

  • [ ]

How to test this PR locally

  • Checkout this branch
  • Run Logstash configuring this plugin in Gemfile:
    • gem "logstash-input-gelf", :path => "/path_to/logstash-input-gelf"
    • bin/logstash-plugin install --no-verify
  • configure a pipeline.conf as:
input {
  gelf { port => "3333" }
}

output { 
  stdout { 
    codec => rubydebug { metadata => true } 
  }
}
  • provide some input from another shell:
echo -n '{ "version": "1.1", "host": "example.org", "short_message": "A short message", "level": 5, "_@timestamp": "foo" }' | nc -u -w0 127.0.0.1 3333
  • Logstash skip the message and doesn't crash

Related issues

Use cases

A user send GELF messages in the form:

{ "version": "1.1", "host": "example.org", "short_message": "A short message", "level": 5, "_@timestamp": "foo" }

and the parsing of such payload can't crash Logstash.

Logs

[2021-11-16T09:22:14,240][WARN ][org.logstash.Event       ][main][a966e44cc4978c04ea63b0e0fba28400454ae6cbcc52f24abaade32528a0a352] Unrecognized @timestamp value type=class org.jruby.RubyFixnum
warning: thread "Ruby-0-Thread-34: :1" terminated with exception (report_on_exception is true):
TypeError: wrong argument type Integer (expected LogStash::Timestamp)
                       set at org/logstash/ext/JrubyEventExtLibrary.java:122
  strip_leading_underscore at /home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:273
                      each at org/jruby/RubyArray.java:1821
  strip_leading_underscore at /home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:271
              udp_listener at /home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:210
                       run at /home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:79
[2021-11-16T09:22:14,265][ERROR][logstash.javapipeline    ][main][a966e44cc4978c04ea63b0e0fba28400454ae6cbcc52f24abaade32528a0a352] A plugin had an unrecoverable error. Will restart this plugin.
  Pipeline_id:main
  Plugin: <LogStash::Inputs::Gelf port=>3333, id=>"a966e44cc4978c04ea63b0e0fba28400454ae6cbcc52f24abaade32528a0a352", enable_metric=>true, codec=><LogStash::Codecs::Plain id=>"plain_26d156d8-c393-41b3-8cde-08392c473c27", enable_metric=>true, charset=>"UTF-8">, host=>"0.0.0.0", remap=>true, strip_leading_underscore=>true, use_tcp=>false, use_udp=>true>
  Error: wrong argument type Integer (expected LogStash::Timestamp)
  Exception: TypeError
  Stack: org/logstash/ext/JrubyEventExtLibrary.java:122:in `set'
/home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:273:in `block in strip_leading_underscore'
org/jruby/RubyArray.java:1821:in `each'
/home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:271:in `strip_leading_underscore'
/home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:210:in `udp_listener'
/home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:79:in `block in run'
[2021-11-16T09:22:14,314][FATAL][logstash.runner          ] An unexpected error occurred! {:error=>#<TypeError: wrong argument type Integer (expected LogStash::Timestamp)>, :backtrace=>["org/logstash/ext/JrubyEventExtLibrary.java:122:in `set'", "/home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:273:in `block in strip_leading_underscore'", "org/jruby/RubyArray.java:1821:in `each'", "/home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:271:in `strip_leading_underscore'", "/home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:210:in `udp_listener'", "/home/andrea/workspace/logstash_andsel/vendor/bundle/jruby/2.5.0/gems/logstash-input-gelf-3.3.0/lib/logstash/inputs/gelf.rb:79:in `block in run'"]}
[2021-11-16T09:22:14,322][FATAL][org.logstash.Logstash    ] Logstash stopped processing because of an error: (SystemExit) exit
org.jruby.exceptions.SystemExit: (SystemExit) exit
	at org.jruby.RubyKernel.exit(org/jruby/RubyKernel.java:747) ~[jruby-complete-9.2.20.0.jar:?]
	at org.jruby.RubyKernel.exit(org/jruby/RubyKernel.java:710) ~[jruby-complete-9.2.20.0.jar:?]
	at home.andrea.workspace.logstash_andsel.lib.bootstrap.environment.<main>(/home/andrea/workspace/logstash_andsel/lib/bootstrap/environment.rb:94) ~[?:?]

@cla-checker-service

cla-checker-service Bot commented Mar 30, 2021 •

Copy link
Copy Markdown

💚 CLA has been signed

@wpjunior
wpjunior force-pushed the fix/crash-by-bigdecimal branch from 4c9b413 to 572852f Compare March 30, 2021 20:43
@wpjunior
wpjunior force-pushed the fix/crash-by-bigdecimal branch from 572852f to 1ececbd Compare March 30, 2021 20:47
@pedrokiefer

Copy link
Copy Markdown

@jsvd any chance that this PR gets merged?

@jsvd

jsvd commented Jul 12, 2021

Copy link
Copy Markdown
Member

Hi folks, what if we just handled this special case: if the stripped key is "@timestamp", convert the value to LogStash::Timestamp, and if that fails, convert it to BigDecimal? For example:

 def strip_leading_underscore(event)
     # Map all '_foo' fields to simply 'foo'
     event.to_hash.keys.each do |key|
       next unless key[0,1] == "_"
       new_key = key[1..-1]
       value = event.get(key)
       if new_key == LogStash::Event::TIMESTAMP
         event.set(new_key, coerce_timestamp_carefully(value))
       else
         event.set(new_key, value)
       end
       event.remove(key)
     end
  end

  def coerce_timestamp_carefully(value)
    LogStash::Timestamp.at(value)
  rescue TypeError => e
    # maybe it's a BigDecimal?
    coerced_value = BigDecimal.new(value)
    LogStash::Timestamp.at(coerced_value)
  end

@andsel

andsel commented Nov 15, 2021

Copy link
Copy Markdown

I think could be the right path but also the coercion of the BigDecimal could make the pipeline crash. Considering the piece:

coerced_value = BigDecimal.new(value)

if value is a malformed String for example "foo" then it raises a NumberFormatException so in that case the event should be dropped

Comment thread lib/logstash/inputs/gelf.rb Outdated
strip_leading_underscore(event) if @strip_leading_underscore
rescue => e
if !stop?
@logger.error("Caught exception while striping leading underscores", :exception => e)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that here we should be more consistent with the TCP counter part, so I would output warn log in form:

@logger.warn("Gelf (udp): striping leading underscores failed.", :exception => ex, :backtrace => ex.backtrace

@andsel andsel added the bug label Nov 16, 2021

@karenzone karenzone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a wording suggestion for consideration for the changelog entry. This is the text that will be picked up and added to our release notes.

Comment thread CHANGELOG.md Outdated

@yaauie yaauie left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it would be good to avoid crashing the input/pipeline, I would prefer an approach that doesn't drop the offending events, perhaps by making the strip_leading_underscores more granular.

The following, for example, will emit the event with fields that could not be renamed in-tact, along with an actionable event tag.

def strip_leading_underscore(event)
  event.to_hash.keys.each do |key|
    move_field(event, key, key.slice(1..-1)) if key.starts_with?('_')
  end
end

def move_field(event, source_field, destination_field)
  value = event.get(source_field)
  value = coerce_timestamp_carefully(value) if new_field == LogStash::Event::TIMESTAMP
  event.set(destination_field, value)
  event.remove(source_field)
rescue => e
  logger.warn("Failed to move field `#{source_field}` to `#{destination_field}`: #{e.message}")
  event.tag("_gelf_move_field_failure", :exception => e.message)
end

Comment thread lib/logstash/inputs/gelf.rb Outdated

def coerce_timestamp_carefully(value)
LogStash::Timestamp.at(value)
rescue TypeError => e

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we possibly be less reactive, routing to BigDecimal if we have a reasonable expectation that doing so will succeed?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if I understand correctly what you mean, instead of reaching a TypeError raised when value is a String and then fallback to BigDecimal, convert it directly to BigDecimal like:

def coerce_timestamp_carefully(value)
  coerced_value = BigDecimal.new(value)
  LogStash::Timestamp.at(coerced_value.to_i, coerced_value.frac * 1000000)
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to pre-classify inputs that will fail when given to LogStash::Timestamp::at, but will succeed at being parsed into a BigDecimal, without being reactive to exceptions?

What inputs are we actually receiving that are causing the crash (that is now mitigated by the safe field-by-field moving)?

For example, we know that big decimal numbers are often encoded as strings to avoid the loss of precision associated with encoding them as floats/doubles, and that strings will be rejected by LogStash::Timestamp::at. We could use this information to wrap epoch-looking strings in BigDecimal proactively, which would result in defining our coerce_timestamp_carefully as something like:

  def coerce_timestamp_carefully(value)
    value = BigDecimal.new(value) if value.kind_of?(String) && value.match(/\A[0-9]+(\.[0-9]+)?\z/)
    LogStash::Timestamp.at(value)
  end

Note that the reason we had to split a BigDecimal up into its whole-seconds and fractions was due to a very old bug in JRuby that was mitigated in Logstash as early as 5.0.0.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated with the suggestion, switched the regexp to accept also numbers in exponential notation like 0.123456e3
adding the part:

0\.[0-9]+e[0-9]

Co-authored-by: Karen Metts <35154725+karenzone@users.noreply.github.com>
@andsel

andsel commented Nov 17, 2021

Copy link
Copy Markdown

@yaauie I like the idea to tag the event instead of dropping with log.
I think we can't do event.tag("_gelf_move_field_failure", "<reason>") according to https://github.com/elastic/logstash/blob/65e163fc5b5fa6a9d25a2272b3d075f112a8cb61/logstash-core/src/main/java/org/logstash/ext/JrubyEventExtLibrary.java#L285-L291 so should we add another subfield in @metadata to fill with the reason?

@andsel
andsel force-pushed the fix/crash-by-bigdecimal branch from 9ef80ad to 5e0b4a1 Compare November 17, 2021 13:17
@andsel
andsel requested a review from yaauie November 17, 2021 15:05
Comment thread lib/logstash/inputs/gelf.rb Outdated

def coerce_timestamp_carefully(value)
LogStash::Timestamp.at(value)
rescue TypeError => e

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to pre-classify inputs that will fail when given to LogStash::Timestamp::at, but will succeed at being parsed into a BigDecimal, without being reactive to exceptions?

What inputs are we actually receiving that are causing the crash (that is now mitigated by the safe field-by-field moving)?

For example, we know that big decimal numbers are often encoded as strings to avoid the loss of precision associated with encoding them as floats/doubles, and that strings will be rejected by LogStash::Timestamp::at. We could use this information to wrap epoch-looking strings in BigDecimal proactively, which would result in defining our coerce_timestamp_carefully as something like:

  def coerce_timestamp_carefully(value)
    value = BigDecimal.new(value) if value.kind_of?(String) && value.match(/\A[0-9]+(\.[0-9]+)?\z/)
    LogStash::Timestamp.at(value)
  end

Note that the reason we had to split a BigDecimal up into its whole-seconds and fractions was due to a very old bug in JRuby that was mitigated in Logstash as early as 5.0.0.

Comment thread lib/logstash/inputs/gelf.rb Outdated
Comment thread spec/inputs/gelf_spec.rb Outdated
Comment thread spec/inputs/gelf_spec.rb Outdated

before(:each) do
subject.register
Thread.new { subject.run(queue) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing our input get shut down anywhere.

Comment thread spec/inputs/gelf_spec.rb Outdated
Comment thread spec/inputs/gelf_spec.rb Outdated
Co-authored-by: Ry Biesemeyer <yaauie@users.noreply.github.com>
@andsel
andsel force-pushed the fix/crash-by-bigdecimal branch from ef0a46f to 55c6d68 Compare November 22, 2021 09:21
@andsel
andsel requested a review from yaauie November 22, 2021 18:07

@yaauie yaauie left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR has become several distinct things:

  1. prevent a crash when renaming _@timestamp to @timestamp: this is mitigated by our field-by-field rename, where we catch exceptions and emit the event with a tag
  2. allow a source _@timestamp encoded as a String to be handled as a BigDecimal: this is mostly-handled, although I have left a comment about avoiding adding support for e-notation until we need it.
  3. prevent Float-encoded timestamps from injecting false precision: this is not addressed in the implementation at all, but instead this PR changes our specification, removing the spec for how we should behave with Floats (that we do receive in practice) and substituting for it a spec for how we should behave with Rationals (which we do not receive in practice). I've left notes in how we can address this.

Comment thread lib/logstash/inputs/gelf.rb Outdated
Comment thread spec/inputs/gelf_spec.rb Outdated
Comment thread lib/logstash/inputs/gelf.rb
Comment thread lib/logstash/inputs/gelf.rb Outdated
Comment thread spec/inputs/gelf_spec.rb Outdated
Co-authored-by: Ry Biesemeyer <yaauie@users.noreply.github.com>
@andsel
andsel force-pushed the fix/crash-by-bigdecimal branch from 72c79e2 to 590b30e Compare November 23, 2021 16:21

@yaauie yaauie left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍🏼

@andsel
andsel merged commit f9de73c into logstash-plugins:main Dec 21, 2021
@mfilocha

Copy link
Copy Markdown

This fix very likely breaks logstash: #70

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants