bugsnag/bugsnag-ruby

Bug: Do not serialize gigantic objects

johnnyshields opened this issue · 7 comments

Describe the bug

If you make an object with a large instance variable, then raise an error, Bugsnag will attempt to serialize the large instance variable with no regard to memory. This caused OOM errors on my machine. There should be some timeout or reasonable limit here.

Steps to reproduce

# assuming Bugsnag installed...

class BigFoo
  def initialize
    @big_data = "a" * 10_000_000_000
  end

  def raise_an_error
    raise RuntimeError
  end
end

BigFoo.new.raise_an_error

Environment

  • Ruby version: 3.0
  • Bugsnag version: 6.23.0

Hi @johnnyshields - I think you originally opened this in bugsnag-api-ruby by mistake so I transferred the issue to bugsnag-ruby instead.

I don't think there's an issue with Bugsnag here, however. I think the OOM will be occurring when the instance is created (i.e. on BigFoo.new), which has nothing to do with Bugsnag, nor will Bugsnag be trying to serialize anything at this point. In fact - Bugsnag shouldn't be serializing this variable at all, unless you're adding it as metadata somewhere else in your code?

I haven't investigated in detail, however, in our app we had a report job object that contains large instance variables. The app was completely fine until a Bugsnag occured (i.e. after instance variables were populated with data), at which point we got OOM presumably because Bugsnag was reporting the instance variables.

@bomberby can you provide more info here?

A closer code representation will be something along these line

class DataObject
  # Heavy object with many child classes
  def inspect
    'a' * 100
  end
end
class BigFoo
  def initialize
    @big_data = []
    10_000_000.times do 
      @big_data << DataObject.new
    end
  end

  def raise_an_error
    bad_method_exception
  end
end

BigFoo.new.raise_an_error

The class that raise the exception has thousands of activerecord models, so when exception is reported, all child objects are added to the report.

Hi @bomberby

We're struggling to reproduce the issue you're seeing, even with your latest code snippets. Are you saying Bugsnag is automatically capturing this data with a default configuration? I think we need to understand exactly what you're doing that causes these large instance variables to be added to the report.

Can you share a more complete reproduction example?

When exception is raised, when resolving the creator class, it will call inspect to all embedded classes individually.
In this case DataObject will receive an inspect message, if that object's inspect is time consuming(such as having it's own embedded object that is also heavy and take a while to serialize), this can easily get out of hand if the exception caller had tens of thousands of objects, and each object is a full database model with ability and data, and perhaps even embedded models.

@bomberby, if you removed Bugsnag from the equation, the call to BigFoo.new.raise_an_error would still call initialize, and therefore iterate 1M times over DataObject. The only way this would get added to your Bugsnag error report and serialized is if you told Bugsnag to do so, e.g. by adding the instance variable @big_data to your reports.

If you've a code snippet that suggest otherwise, please do let us know. If you run:

# configure Bugsnag here...

class DataObject
  # Heavy object with many child classes
  def inspect
    'a' * 100
  end
end

class BigFoo
  def initialize
    @big_data = []
    10.times do 
      @big_data.append(DataObject.new)
    end
  end

  def raise_an_error
    bad_method_exception
  end
end

b = BigFoo.new
puts b.instance_variable_get(:@big_data)
### Optionally add @big_data to a Bugsnag report
# Bugsnag.add_metadata(:big_data, {
#   big_data: b.instance_variable_get(:@big_data).inspect
# })
b.raise_an_error

You'll see the output that @big_data exists...

> $ bundle exec ruby big-objects.rb                                                                  ⬡ 14.15.4 [±master ●●]
#<DataObject:0x00007f96ff20f498>
#<DataObject:0x00007f96ff20f420>
#<DataObject:0x00007f96ff20f3f8>
#<DataObject:0x00007f96ff20f3d0>
#<DataObject:0x00007f96ff20f3a8>
#<DataObject:0x00007f96ff20f380>
#<DataObject:0x00007f96ff20f358>
#<DataObject:0x00007f96ff20f308>
#<DataObject:0x00007f96ff20f2e0>
#<DataObject:0x00007f96ff20f2b8>
** [Bugsnag] 2022-01-17 15:02:05 +0000: Notifying https://notify.bugsnag.com of NameError
** [Bugsnag] 2022-01-17 15:02:05 +0000: Waiting for 1 outstanding request(s)
Traceback (most recent call last):
        1: from big-objects.rb:31:in `<main>'
big-objects.rb:25:in `raise_an_error': undefined local variable or method `bad_method_exception' for #<BigFoo:0x00007f96ff20f4e8> (NameError)

... but, it's not part of the Bugsnag payload

image

We're closing this now as we haven't been able to reproduce the problem, but if you can share an example we'd be happy to reopen and investigate further.