Skip to content

Commit a0f92ce

Browse files
committed
Update module with review
2 parents 7358bcf + dff6af0 commit a0f92ce

File tree

36 files changed

+1551
-784
lines changed

36 files changed

+1551
-784
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ GEM
123123
metasploit-model (0.28.0)
124124
activesupport
125125
railties (< 4.0.0)
126-
metasploit_data_models (0.21.1)
126+
metasploit_data_models (0.21.2)
127127
activerecord (>= 3.2.13, < 4.0.0)
128128
activesupport
129129
arel-helpers

Rakefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ Metasploit::Framework::Require.optionally_active_record_railtie
1111

1212
Metasploit::Framework::Application.load_tasks
1313
Metasploit::Framework::Spec::Constants.define_task
14+
Metasploit::Framework::Spec::Threads::Suite.define_task
1415
Metasploit::Framework::Spec::UntestedPayloads.define_task

config/boot.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,13 @@
2323
end
2424

2525
begin
26-
require 'bundler'
26+
require 'bundler/setup'
2727
rescue LoadError
2828
$stderr.puts "[*] Metasploit requires the Bundler gem to be installed"
2929
$stderr.puts " $ gem install bundler"
30-
exit(0)
30+
exit(1)
3131
end
3232

33-
Bundler.setup
34-
3533
lib_path = root.join('lib').to_path
3634

3735
unless $LOAD_PATH.include? lib_path

lib/metasploit/framework.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module Framework
3535
extend ActiveSupport::Autoload
3636

3737
autoload :Spec
38+
autoload :ThreadFactoryProvider
3839

3940
# Returns the root of the metasploit-framework project. Use in place of
4041
# `Rails.root`.

lib/metasploit/framework/spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ module Metasploit::Framework::Spec
22
extend ActiveSupport::Autoload
33

44
autoload :Constants
5+
autoload :Threads
56
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module Metasploit::Framework::Spec::Threads
2+
extend ActiveSupport::Autoload
3+
4+
autoload :Suite
5+
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#
2+
# Standard Library
3+
#
4+
5+
require 'securerandom'
6+
7+
#
8+
# Project
9+
#
10+
11+
require 'metasploit/framework/spec/threads/suite'
12+
13+
original_thread_new = Thread.method(:new)
14+
15+
# Patches `Thread.new` so that if logs `caller` so thread leaks can be traced
16+
Thread.define_singleton_method(:new) { |*args, &block|
17+
uuid = SecureRandom.uuid
18+
# tag caller with uuid so that only leaked threads caller needs to be printed
19+
lines = ["BEGIN Thread.new caller (#{uuid})"]
20+
21+
caller.each do |frame|
22+
lines << " #{frame}"
23+
end
24+
25+
lines << 'END Thread.new caller'
26+
27+
Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.parent.mkpath
28+
29+
Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.open('a') { |f|
30+
# single puts so threads can't write in between each other.
31+
f.puts lines.join("\n")
32+
}
33+
34+
options = {original_args: args, uuid: uuid}
35+
36+
original_thread_new.call(options) {
37+
# record uuid for thread-leak detection can used uuid to correlate log with this thread.
38+
Thread.current[Metasploit::Framework::Spec::Threads::Suite::UUID_THREAD_LOCAL_VARIABLE] = options.fetch(:uuid)
39+
block.call(*options.fetch(:original_args))
40+
}
41+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
require 'pathname'
2+
3+
# @note needs to use explicit nesting. so this file can be loaded directly without loading 'metasploit/framework' which
4+
# allows for faster loading of rake tasks.
5+
module Metasploit
6+
module Framework
7+
module Spec
8+
module Threads
9+
module Suite
10+
#
11+
# CONSTANTS
12+
#
13+
14+
# Number of allowed threads when threads are counted in `after(:suite)` or `before(:suite)`
15+
EXPECTED_THREAD_COUNT_AROUND_SUITE = 1
16+
# `caller` for all Thread.new calls
17+
LOG_PATHNAME = Pathname.new('log/metasploit/framework/spec/threads/suite.log')
18+
# Regular expression for extracting the UUID out of {LOG_PATHNAME} for each Thread.new caller block
19+
UUID_REGEXP = /BEGIN Thread.new caller \((?<uuid>.*)\)/
20+
# Name of thread local variable that Thread UUID is stored
21+
UUID_THREAD_LOCAL_VARIABLE = "metasploit/framework/spec/threads/logger/uuid"
22+
23+
#
24+
# Module Methods
25+
#
26+
27+
# Configures `before(:suite)` and `after(:suite)` callback to detect thread leaks.
28+
#
29+
# @return [void]
30+
def self.configure!
31+
unless @configured
32+
RSpec.configure do |config|
33+
config.before(:suite) do
34+
thread_count = Metasploit::Framework::Spec::Threads::Suite.non_debugger_thread_list.count
35+
36+
# check with if first so that error message can be constructed lazily
37+
if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE
38+
# LOG_PATHNAME may not exist if suite run without `rake spec`
39+
if LOG_PATHNAME.exist?
40+
log = LOG_PATHNAME.read()
41+
else
42+
log "Run `rake spec` to log where Thread.new is called."
43+
end
44+
45+
raise RuntimeError,
46+
"#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when " \
47+
"only #{EXPECTED_THREAD_COUNT_AROUND_SUITE} " \
48+
"#{'thread'.pluralize(EXPECTED_THREAD_COUNT_AROUND_SUITE)} expected before suite runs:\n" \
49+
"#{log}"
50+
end
51+
52+
LOG_PATHNAME.parent.mkpath
53+
54+
LOG_PATHNAME.open('a') do |f|
55+
# separator so after(:suite) can differentiate between threads created before(:suite) and during the
56+
# suites
57+
f.puts 'before(:suite)'
58+
end
59+
end
60+
61+
config.after(:suite) do
62+
LOG_PATHNAME.parent.mkpath
63+
64+
LOG_PATHNAME.open('a') do |f|
65+
# separator so that a flip flop can be used when reading the file below. Also useful if it turns
66+
# out any threads are being created after this callback, which could be the case if another
67+
# after(:suite) accidentally created threads by creating an Msf::Simple::Framework instance.
68+
f.puts 'after(:suite)'
69+
end
70+
71+
thread_list = Metasploit::Framework::Spec::Threads::Suite.non_debugger_thread_list
72+
thread_count = thread_list.count
73+
74+
if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE
75+
error_lines = []
76+
77+
if LOG_PATHNAME.exist?
78+
caller_by_thread_uuid = Metasploit::Framework::Spec::Threads::Suite.caller_by_thread_uuid
79+
80+
thread_list.each do |thread|
81+
thread_uuid = thread[Metasploit::Framework::Spec::Threads::Suite::UUID_THREAD_LOCAL_VARIABLE]
82+
83+
# unmanaged thread, such as the main VM thread
84+
unless thread_uuid
85+
next
86+
end
87+
88+
caller = caller_by_thread_uuid[thread_uuid]
89+
90+
error_lines << "Thread #{thread_uuid}'s status is #{thread.status.inspect} " \
91+
"and was started here:\n"
92+
93+
error_lines.concat(caller)
94+
end
95+
else
96+
error_lines << "Run `rake spec` to log where Thread.new is called."
97+
end
98+
99+
raise RuntimeError,
100+
"#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when only " \
101+
"#{EXPECTED_THREAD_COUNT_AROUND_SUITE} " \
102+
"#{'thread'.pluralize(EXPECTED_THREAD_COUNT_AROUND_SUITE)} expected after suite runs:\n" \
103+
"#{error_lines.join}"
104+
end
105+
end
106+
end
107+
108+
@configured = true
109+
end
110+
111+
@configured
112+
end
113+
114+
def self.define_task
115+
Rake::Task.define_task('metasploit:framework:spec:threads:suite') do
116+
if Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.exist?
117+
Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.delete
118+
end
119+
120+
parent_pathname = Pathname.new(__FILE__).parent
121+
threads_logger_pathname = parent_pathname.join('logger')
122+
load_pathname = parent_pathname.parent.parent.parent.parent.expand_path
123+
124+
# Must append to RUBYOPT or Rubymine debugger will not work
125+
ENV['RUBYOPT'] = "#{ENV['RUBYOPT']} -I#{load_pathname} -r#{threads_logger_pathname}"
126+
end
127+
128+
Rake::Task.define_task(spec: 'metasploit:framework:spec:threads:suite')
129+
end
130+
131+
# @note Ensure {LOG_PATHNAME} exists before calling.
132+
#
133+
# Yields each line of {LOG_PATHNAME} that happened during the suite run.
134+
#
135+
# @yield [line]
136+
# @yieldparam line [String] a line in the {LOG_PATHNAME} between `before(:suite)` and `after(:suite)`
137+
# @yieldreturn [void]
138+
def self.each_suite_line
139+
in_suite = false
140+
141+
LOG_PATHNAME.each_line do |line|
142+
if in_suite
143+
if line.start_with?('after(:suite)')
144+
break
145+
else
146+
yield line
147+
end
148+
else
149+
if line.start_with?('before(:suite)')
150+
in_suite = true
151+
end
152+
end
153+
end
154+
end
155+
156+
# @note Ensure {LOG_PATHNAME} exists before calling.
157+
#
158+
# Yield each line for each Thread UUID gathered during the suite run.
159+
#
160+
# @yield [uuid, line]
161+
# @yieldparam uuid [String] the UUID of thread thread
162+
# @yieldparam line [String] a line in the `caller` for the given `uuid`
163+
# @yieldreturn [void]
164+
def self.each_thread_line
165+
in_thread_caller = false
166+
uuid = nil
167+
168+
each_suite_line do |line|
169+
if in_thread_caller
170+
if line.start_with?('END Thread.new caller')
171+
in_thread_caller = false
172+
next
173+
else
174+
yield uuid, line
175+
end
176+
else
177+
match = line.match(UUID_REGEXP)
178+
179+
if match
180+
in_thread_caller = true
181+
uuid = match[:uuid]
182+
end
183+
end
184+
end
185+
end
186+
187+
# The `caller` for each Thread UUID.
188+
#
189+
# @return [Hash{String => Array<String>}]
190+
def self.caller_by_thread_uuid
191+
lines_by_thread_uuid = Hash.new { |hash, uuid|
192+
hash[uuid] = []
193+
}
194+
195+
each_thread_line do |uuid, line|
196+
lines_by_thread_uuid[uuid] << line
197+
end
198+
199+
lines_by_thread_uuid
200+
end
201+
202+
# @return
203+
def self.non_debugger_thread_list
204+
Thread.list.reject { |thread|
205+
# don't do `is_a? Debugger::DebugThread` because it requires Debugger::DebugThread to be loaded, which it
206+
# won't when not debugging.
207+
thread.class.name == 'Debugger::DebugThread'
208+
}
209+
end
210+
end
211+
end
212+
end
213+
end
214+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Wraps {Msf::Framework} so that {Msf::Framework#threads} is only created on the first call to {#spawn} by
2+
# {Rex::ThreadFactory#spawn}, which allows the threads used by {Msf::ThreadManager} to be created lazily.
3+
#
4+
# @example Setting Rex::ThreadFactory.provider and spawning threads
5+
# Rex::ThreadFactory.provider = Metasploit::Framework::ThreadFactoryProvider.new(framework: framework)
6+
# # framework.threads created here
7+
# Rex::ThreadFactory.spawn("name", false) { ... }
8+
#
9+
class Metasploit::Framework::ThreadFactoryProvider < Metasploit::Model::Base
10+
#
11+
# Attributes
12+
#
13+
14+
# @!attribute framework
15+
# The framework managing the spawned threads.
16+
#
17+
# @return [Msf::Framework]
18+
attr_accessor :framework
19+
20+
# Spawns a thread monitored by {Msf::ThreadManager} in {Msf::Framework#threads}.
21+
#
22+
# (see Msf::ThreadManager#spawn)
23+
def spawn(name, critical, *args, &block)
24+
framework.threads.spawn(name, critical, *args, &block)
25+
end
26+
end

lib/msf/core/db_manager.rb

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
require 'msf/core/db_import_error'
1818
require 'msf/core/host_state'
1919
require 'msf/core/service_state'
20-
require 'msf/core/task_manager'
2120

2221
# The db module provides persistent storage and events. This class should be instantiated LAST
2322
# as the active_suppport library overrides Kernel.require, slowing down all future code loads.
@@ -47,7 +46,6 @@ class Msf::DBManager
4746
autoload :Service, 'msf/core/db_manager/service'
4847
autoload :Session, 'msf/core/db_manager/session'
4948
autoload :SessionEvent, 'msf/core/db_manager/session_event'
50-
autoload :Sink, 'msf/core/db_manager/sink'
5149
autoload :Task, 'msf/core/db_manager/task'
5250
autoload :Vuln, 'msf/core/db_manager/vuln'
5351
autoload :VulnAttempt, 'msf/core/db_manager/vuln_attempt'
@@ -80,7 +78,6 @@ class Msf::DBManager
8078
include Msf::DBManager::Service
8179
include Msf::DBManager::Session
8280
include Msf::DBManager::SessionEvent
83-
include Msf::DBManager::Sink
8481
include Msf::DBManager::Task
8582
include Msf::DBManager::Vuln
8683
include Msf::DBManager::VulnAttempt
@@ -160,11 +157,6 @@ def initialize_database_support
160157
#
161158
initialize_adapter
162159

163-
#
164-
# Instantiate the database sink
165-
#
166-
initialize_sink
167-
168160
true
169161
end
170162

0 commit comments

Comments
 (0)