Skip to content

Instantly share code, notes, and snippets.

@todb-r7
Created May 17, 2012 19:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save todb-r7/2721110 to your computer and use it in GitHub Desktop.
Save todb-r7/2721110 to your computer and use it in GitHub Desktop.
Troubleshooting some rails memory leak
The problem: the first seven hosts of the poisoned nmap file gets you to 10.3 to 10.5% mem usage (for 8gb memory) from the sample file.
The first order of business is to get a sample file to be usable for test cases without actually crashing ruby, and to make the host info more useful for debugging (sequential IPs, fake target domain and names, basic attempts at anonymizing the real data).
top - 10:51:26 up 59 min, 7 users, load average: 0.46, 0.57, 0.47
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
Cpu(s): 3.8%us, 4.0%sy, 0.0%ni, 92.1%id, 0.0%wa, 0.0%hi, 0.2%si, 0.0%st
Mem: 8161728k total, 3880048k used, 4281680k free, 341184k buffers
Swap: 9535480k total, 0k used, 9535480k free, 1092928k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
6697 todb 20 0 1046m 829m 6060 S 0 10.4 1:41.13 ruby
Importing off of Metasploit 4.2.0 (svn r14791 updated 85 days ago (2012.02.22)) has no such problem:
top - 10:54:07 up 1:01, 8 users, load average: 0.37, 0.50, 0.45
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
Cpu(s): 3.5%us, 0.8%sy, 0.0%ni, 95.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 8161728k total, 3236764k used, 4924964k free, 351060k buffers
Swap: 9535480k total, 0k used, 9535480k free, 1119124k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
7244 todb 20 0 366m 153m 6084 S 0 1.9 0:24.63 ruby
First WAG: Just garbage collect after every host by adding a GC.new into the report_host() call. This has no effect.
Research attempt: Put together an ObjectSpace checker
Rails3 metasploit:
[*] resource (/home/todb/rc/nmap.rc)> Ruby Code (33 bytes)
String 3246022
RubyVM::InstructionSequence 33178
Array 15609
Hash 7151
Regexp 4855
Class 4590
Module 2684
RubyVM::Env 2338
Proc 2101
Msf::OptBool 515
Rails2 metasploit:
[*] resource (/home/todb/rc/nmap.rc)> Ruby Code (33 bytes)
String 304973
RubyVM::InstructionSequence 28968
Array 13479
Hash 4935
Regexp 4606
Class 4338
Module 2466
Proc 1566
RubyVM::Env 1282
Msf::OptBool 501
So there are 11x the strings in Rails3. Wow. Wonder what those are.
Let's take a look at what the ObjectSpace looks like after importing each host. Adding
diff --git a/lib/msf/core/db.rb b/lib/msf/core/db.rb
index a3065aa..124ec15 100644
--- a/lib/msf/core/db.rb
+++ b/lib/msf/core/db.rb
@@ -347,6 +347,9 @@ class DBManager
host.save!
end
+ # XXX Delete this debug stuff
+ load '/tmp/ospace.rb'
+ ::DebugTodb.count_objs
host
}
end
After every host, you add something like 800,000 strings to the heap. wow!
[*] Importing 'Nmap XML' data
[*] Import: Parsing with 'Nokogiri v1.5.2'
String 390522
RubyVM::InstructionSequence 33095
Array 15560
Hash 5609
Regexp 4854
Class 4592
Module 2684
RubyVM::Env 2320
Proc 2087
Msf::OptBool 515
[*] Importing host 118.21.1.1
String 1322786
Hash 307942
Mdm::Service 49138
Array 33694
RubyVM::InstructionSequence 33219
Regexp 4855
Class 4592
Module 2685
RubyVM::Env 2379
Proc 2140
[*] Importing host 118.21.1.2
String 2209456
Hash 609595
Mdm::Service 98276
Array 44338
RubyVM::InstructionSequence 33215
Regexp 4855
Class 4592
Time 3271
Module 2685
RubyVM::Env 2352
[*] Importing host 118.21.1.3
String 3096813
Hash 912149
Mdm::Service 147414
Array 57831
RubyVM::InstructionSequence 33215
Regexp 4855
Time 4839
Class 4592
ActiveRecord::Relation 2867
Arel::Table 2845
[*] Importing host 118.21.1.4
String 4016862
Hash 1214644
Mdm::Service 196552
Array 71959
RubyVM::InstructionSequence 33215
Time 6427
Regexp 4855
Class 4592
ActiveRecord::Relation 3816
Arel::Table 3785
[*] Importing host 118.21.1.5
String 4911826
Hash 1517046
Mdm::Service 245690
Array 85596
RubyVM::InstructionSequence 33215
Time 7995
Regexp 4855
ActiveRecord::Relation 4750
Arel::Table 4725
Arel::Attributes::Attribute 4721
[*] Importing host 118.21.1.6
String 5815178
Hash 1819520
Mdm::Service 294828
Array 98979
RubyVM::InstructionSequence 33215
Time 9569
ActiveRecord::Relation 5684
Arel::Table 5665
Arel::Attributes::Attribute 5659
Arel::Nodes::Equality 5647
[*] Importing host 118.21.1.7
[*] Successfully imported /tmp/nmap.pro.poison.xml
Compare to 4.2.0:
[*] Importing 'Nmap XML' data
[*] Import: Parsing with 'Nokogiri v1.5.2'
String 340666
RubyVM::InstructionSequence 28905
Array 13606
Hash 5298
Regexp 4607
Class 4338
Module 2467
Proc 1570
RubyVM::Env 1288
Msf::OptBool 501
[*] Importing host 118.21.1.1
String 394339
RubyVM::InstructionSequence 28975
Array 13574
Hash 5309
Regexp 4607
Class 4338
Module 2467
Proc 1573
RubyVM::Env 1302
Msf::OptBool 501
[*] Importing host 118.21.1.2
String 398396
RubyVM::InstructionSequence 28970
Array 13523
Hash 5307
Regexp 4606
Class 4338
Module 2467
Proc 1570
RubyVM::Env 1288
Msf::OptBool 501
[*] Importing host 118.21.1.3
String 397932
RubyVM::InstructionSequence 28970
Array 13520
Hash 5314
Regexp 4606
Class 4338
Module 2467
Proc 1570
RubyVM::Env 1288
Msf::OptBool 501
[*] Importing host 118.21.1.4
String 397920
RubyVM::InstructionSequence 28970
Array 13512
Hash 5319
Regexp 4607
Class 4338
Module 2467
Proc 1570
RubyVM::Env 1288
Msf::OptBool 501
[*] Importing host 118.21.1.5
String 398585
RubyVM::InstructionSequence 28970
Array 13502
Hash 5327
Regexp 4606
Class 4338
Module 2467
Proc 1570
RubyVM::Env 1288
Msf::OptBool 501
[*] Importing host 118.21.1.6
String 398459
RubyVM::InstructionSequence 28970
Array 13507
Hash 5323
Regexp 4607
Class 4338
Module 2467
Proc 1570
RubyVM::Env 1288
Msf::OptBool 501
[*] Importing host 118.21.1.7
So, wrote a string inspector:
def eyeball_strings
@strings = {}
ObjectSpace.each_object do |o|
next unless o.kind_of? String
@strings[o[0,60]] ||= 0
@strings[o[0,60]] += 1
end
i = 0
@strings.sort {|a,b| b[1] <=> a[1]}.each do |k,v|
i += 1
puts "*%-60s* %d" % [k,v]
break if i >= 20
end
end
You can see that all these strings are getting created as part of Mdm::Service.
Not super surprising, but now we know for sure. Yay?
resource (/home/todb/rc/nmap.rc)> db_import /tmp/nmap.pro.poison.xml
[*] Importing 'Nmap XML' data
[*] Import: Parsing with 'Nokogiri v1.5.2'
* * 4518
*/home/todb/git/rapid7/metasploit-framework/modules/exploits/* 4323
*/home/todb/git/rapid7/metasploit-framework/modules/auxiliary* 3663
*initialize * 3072
*Payload * 2863
*/home/todb/git/rapid7/metasploit-framework/lib/gemcache/ruby* 2712
*Name * 2447
*Description * 2441
*Author * 2385
*License * 2281
*Version * 2245
*/home/todb/svn/msf3/modules * 2178
*Type * 2086
*FileNames * 2030
*FilePaths * 2028
*$Revision$ * 1988
*FilePaths=/home/todb/svn/msf3/modules * 1893
*Platform * 1813
*: * 1777
*V * 1621
[*] Importing host 118.21.1.1
* * 63081
*847 * 59297
*tcp * 59069
*created_at * 58638
*updated_at * 58609
*id * 58402
*host_id * 57903
*name * 57465
*info * 57384
*state * 57370
*port * 57056
*proto * 56983
*open * 55304
*/home/todb/git/rapid7/metasploit-framework/modules/exploits/* 4232
*unknown * 3615
*/home/todb/git/rapid7/metasploit-framework/lib/gemcache/ruby* 2805
*initialize * 2805
*Payload * 2512
*filtered * 2416
*/home/todb/git/rapid7/metasploit-framework/modules/auxiliary* 2382
[*] Importing host 118.21.1.2
* * 107421
*id * 106336
*updated_at * 105922
*created_at * 105894
*host_id * 104845
*info * 103832
*name * 103817
*state * 103804
*port * 103147
*proto * 103094
*tcp * 102709
*open * 96670
*848 * 52463
*847 * 51736
*unknown * 5901
*filtered * 4289
*/home/todb/git/rapid7/metasploit-framework/modules/exploits/* 4231
*/home/todb/git/rapid7/metasploit-framework/lib/gemcache/ruby* 2810
*initialize * 2722
*Payload * 2511
[*] Importing host 118.21.1.3
* * 159556
*updated_at * 156016
*created_at * 156002
*id * 155124
*tcp * 154262
*host_id * 154022
*info * 152362
*state * 152354
*name * 152287
*port * 151425
*proto * 151383
*open * 145660
*849 * 54075
*848 * 51592
*847 * 50676
*unknown * 9053
*filtered * 6245
*/home/todb/git/rapid7/metasploit-framework/modules/exploits/* 4232
*workspace_id * 3654
*57 * 3588
[*] Importing host 118.21.1.4
*created_at * 207581
*updated_at * 207564
* * 207131
*id * 206433
*host_id * 204962
*info * 202567
*state * 202519
*name * 202416
*port * 201323
*proto * 201252
*tcp * 201155
*open * 190157
*849 * 52196
*850 * 51059
*848 * 50602
*847 * 50502
*unknown * 11578
*filtered * 8044
*workspace_id * 5069
*57 * 4994
[*] Importing host 118.21.1.5
* * 259356
*created_at * 257859
*updated_at * 257849
*id * 256396
*host_id * 254633
*tcp * 252845
*state * 251698
*info * 251669
*name * 251479
*port * 250157
*proto * 250100
*open * 238993
*851 * 53639
*850 * 51711
*849 * 50607
*847 * 50443
*848 * 50433
*unknown * 14612
*filtered * 10203
*workspace_id * 6194
[*] Importing host 118.21.1.6
*updated_at * 310623
*created_at * 310602
*id * 308631
* * 307420
*host_id * 306574
*state * 302894
*info * 302893
*name * 302576
*port * 300991
*proto * 300976
*tcp * 300276
*open * 283848
*851 * 51773
*852 * 51750
*850 * 50489
*848 * 50435
*849 * 50420
*847 * 50415
*unknown * 17248
*filtered * 12214
[*] Importing host 118.21.1.7
[*] Successfully imported /tmp/nmap.pro.poison.xml
[*] resource (/home/todb/rc/nmap.rc)> Ruby Code (45 bytes)
String 3842247
RubyVM::InstructionSequence 33220
Array 15626
Hash 7154
Regexp 4855
Class 4592
Module 2685
RubyVM::Env 2333
Proc 2097
Msf::OptBool 515
msf > irb
[*] Starting IRB shell...
>> DebugTodb.eyeball_strings
*/home/todb/git/rapid7/metasploit-framework/modules/exploits/* 101355
*Payload * 27897
*Platform * 26389
*Name * 24779
*Author * 24773
*Description * 24697
*Version * 24010
*Ret * 23701
*License * 23451
* * 23038
*/home/todb/git/rapid7/metasploit-framework/modules/payloads/* 22757
*initialize * 20430
*$Revision$ * 19908
*V * 17218
*win * 17218
*References * 14836
*/home/todb/git/rapid7/metasploit-framework/lib/gemcache/ruby* 14716
*Targets * 14511
*URL * 14301
*DisclosureDate * 14038
=> nil
>>
Noticed that I have no idea how to actually work on MDM, because that's where the problem is. Futzed around with bundler for a bit, then just started editing in the local gemcache.
Found that the problem is isolated to normalize_host_os -- commenting that out (by inserting a return before info_changed?) solved the problem. Let's dig there now.
Found the problem is in the host.services.each block of normalize_os. Still have too many strings when that whole block is commented out, but it's better. By a lot.
Tried ActiveRecord::Base.uncached {} block around everything in normalize_os. nope.
At this point, went back to Port 80 Engineering, found that ObjectSpace already has count_objects. Mine's a wee bit nicer, though much slower. Whatever. Let's give gdb.rb a shot.
Minor problems with gem install, fixed. Getting a feel for the interface.
Turns out that ruby eval works, but ruby objects does not. So... kinda useless? The eval is nice but not what I want.
It's getting late, so let's just avoid the problem for now. Think about how to make normalize_os switchable. It's okay to renormalize after one or two services are added, but not after every service is added and we have 100s to go through.
Okay so never mind on that! CHanged normalize_os to this:
finger_services = self.services.find(:all, :conditions => [ "name is not null and name != ''" ])
finger_services.each do |s|
...
end
and now no memory leak. let's see if the services are actually being queried tho... yep! Fingerprints are definitely still happening:
address mac name os_name os_flavor os_sp purpose info comments
------- --- ---- ------- --------- ----- ------- ---- --------
118.21.1.1 118-021-235-1.jp.synacktrap.net Apple embedded server
118.21.1.2 118-021-235-2.jp.synacktrap.net Apple embedded server
118.21.1.3 118-021-235-3.jp.synacktrap.net Linksys embedded server
118.21.1.4 118-021-235-4.jp.synacktrap.net Apple embedded server
118.21.1.5 118-021-235-5.jp.synacktrap.net Apple embedded server
118.21.1.6 118-021-235-6.jp.synacktrap.net Linksys embedded server
118.21.1.7 118-021-235-7.jp.synacktrap.net Apple embedded server
This matches the 4.2.0 results. Let's import something a little more exciting...
msf > db_import /home/todb/git/rapid7/qa/sample_data/msfxv3.zip
msf > hosts
Lookin good! This matches all the OS fingerprints from 4.2.0. There are slight differences between the device types, but that's certainly unrelated to this.
Create a diff of my hacks to the local gemcache so I can get them committed proper:
$ diff -Naur ../metasploit_data_models/lib/metasploit_data_models/active_record_models/host.rb lib/gemcache/ruby/1.9.1/gems/metasploit_data_models-0.0.2.43DEV/lib/metasploit_data_models/active_record_models/host.rb > /tmp/out.diff
$ cd ../metasploit_data_models
$ patch -p0 -i /tmp/out.diff
And commit and stick this in a pastie so I don't forget how I did all this.
@masonhale
Copy link

I've been troubleshooting some memory issues in an app that seem to have appeared since upgrading from Rails 2.3 to 3.x. I found this gist in my Google searches, and I am seeing similarly large object counts when I dump my ObjectSpace.

Your gist implies that you found a fix:

  finger_services = self.services.find(:all, :conditions => [ "name is not null and name != ''" ])
  finger_services.each do |s|
      ...
  end

Which is a tantalizing detail.

Did you ever determine what the root cause of the memory bloat was?

Thanks in advance

Found the associated commit here:

rapid7/metasploit_data_models@5032377

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