Module: Metasploit::Framework::Spec::Threads::Suite

Defined in:
lib/metasploit/framework/spec/threads/suite.rb

Constant Summary collapse

EXPECTED_THREAD_COUNT_AROUND_SUITE =

Number of allowed threads when threads are counted in `after(:suite)` or `before(:suite)`

Known threads:

1. Main Ruby thread
2. Active Record connection pool thread
3. Framework thread manager, a monitor thread for removing dead threads
   https://github.com/rapid7/metasploit-framework/blame/04e8752b9b74cbaad7cb0ea6129c90e3172580a2/lib/msf/core/thread_manager.rb#L66-L89
4. Ruby's Timeout library thread, an automatically created monitor thread when using `Thread.timeout(1) { }`
   https://github.com/ruby/timeout/blob/bd25f4b138b86ef076e6d9d7374b159fffe5e4e9/lib/timeout.rb#L129-L137
5. REMOTE_DB thread, if enabled

Intermittent threads that are non-deterministically left behind, which should be fixed in the future:

1. metadata cache hydration
   https://github.com/rapid7/metasploit-framework/blob/115946cd06faccac654e956e8ba9cf72ff328201/lib/msf/core/modules/metadata/cache.rb#L150-L153
2. session manager
   https://github.com/rapid7/metasploit-framework/blob/115946cd06faccac654e956e8ba9cf72ff328201/lib/msf/core/session_manager.rb#L153-L168
ENV['REMOTE_DB'] ? 7 : 6
LOG_PATHNAME =

`caller` for all Thread.new calls

Pathname.new('log/metasploit/framework/spec/threads/suite.log')
UUID_REGEXP =

Regular expression for extracting the UUID out of LOG_PATHNAME for each Thread.new caller block

/BEGIN Thread.new caller \((?<uuid>.*)\)/
UUID_THREAD_LOCAL_VARIABLE =

Name of thread local variable that Thread UUID is stored

"metasploit/framework/spec/threads/logger/uuid"

Class Method Summary collapse

Class Method Details

.caller_by_thread_uuidHash{String => Array<String>}

The `caller` for each Thread UUID.

Returns:

  • (Hash{String => Array<String>})


208
209
210
211
212
213
214
215
216
217
218
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 208

def self.caller_by_thread_uuid
  lines_by_thread_uuid = Hash.new { |hash, uuid|
    hash[uuid] = []
  }

  each_thread_line do |uuid, line|
    lines_by_thread_uuid[uuid] << line
  end

  lines_by_thread_uuid
end

.configure!void

This method returns an undefined value.

Configures `before(:suite)` and `after(:suite)` callback to detect thread leaks.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 47

def self.configure!
  unless @configured
    RSpec.configure do |config|
      config.before(:suite) do
        thread_count = Metasploit::Framework::Spec::Threads::Suite.non_debugger_thread_list.count

        # check with if first so that error message can be constructed lazily
        if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE
          # LOG_PATHNAME may not exist if suite run without `rake spec`
          if LOG_PATHNAME.exist?
            log = LOG_PATHNAME.read()
          else
            log "Run `rake spec` to log where Thread.new is called."
          end

          raise RuntimeError,
                "#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when " \
                "only #{EXPECTED_THREAD_COUNT_AROUND_SUITE} " \
                "#{'thread'.pluralize(EXPECTED_THREAD_COUNT_AROUND_SUITE)} expected before suite runs:\n" \
                "#{log}"
        end

        LOG_PATHNAME.parent.mkpath

        LOG_PATHNAME.open('a') do |f|
          # separator so after(:suite) can differentiate between threads created before(:suite) and during the
          # suites
          f.puts 'before(:suite)'
        end
      end

      config.after(:suite) do
        LOG_PATHNAME.parent.mkpath

        LOG_PATHNAME.open('a') do |f|
          # separator so that a flip flop can be used when reading the file below.  Also useful if it turns
          # out any threads are being created after this callback, which could be the case if another
          # after(:suite) accidentally created threads by creating an Msf::Simple::Framework instance.
          f.puts 'after(:suite)'
        end

        thread_list = Metasploit::Framework::Spec::Threads::Suite.non_debugger_thread_list
        thread_count = thread_list.count

        if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE
          error_lines = []

          if LOG_PATHNAME.exist?
            caller_by_thread_uuid = Metasploit::Framework::Spec::Threads::Suite.caller_by_thread_uuid

            thread_list.each do |thread|
              thread_uuid = thread[Metasploit::Framework::Spec::Threads::Suite::UUID_THREAD_LOCAL_VARIABLE]
              thread_name = thread[:tm_name]

              # unmanaged thread, such as the main VM thread
              unless thread_uuid
                next
              end

              caller = caller_by_thread_uuid[thread_uuid]

              error_lines << "Thread #{thread_uuid}'s (name=#{thread_name} status is #{thread.status.inspect} " \
                             "and was started here:\n"
              error_lines.concat(caller)
              error_lines << "The thread backtrace was:\n#{thread.backtrace ? thread.backtrace.join("\n") : 'nil (no backtrace)'}\n"
            end
          else
            error_lines << "Run `rake spec` to log where Thread.new is called."
          end

          raise RuntimeError,
                "#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when only " \
                "#{EXPECTED_THREAD_COUNT_AROUND_SUITE} " \
                "#{'thread'.pluralize(EXPECTED_THREAD_COUNT_AROUND_SUITE)} expected after suite runs:\n" \
                "#{error_lines.join}"
        end
      end
    end

    @configured = true
  end

  @configured
end

.define_taskObject



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 132

def self.define_task
  Rake::Task.define_task('metasploit:framework:spec:threads:suite') do
    if Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.exist?
      Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.delete
    end

    parent_pathname = Pathname.new(__FILE__).parent
    threads_logger_pathname = parent_pathname.join('logger')
    load_pathname = parent_pathname.parent.parent.parent.parent.expand_path

    # Must append to RUBYOPT or Rubymine debugger will not work
    ENV['RUBYOPT'] = "#{ENV['RUBYOPT']} -I#{load_pathname} -r#{threads_logger_pathname}"
  end

  Rake::Task.define_task(spec: 'metasploit:framework:spec:threads:suite')
end

.each_suite_line {|line| ... } ⇒ Object

Note:

Ensure LOG_PATHNAME exists before calling.

Yields each line of LOG_PATHNAME that happened during the suite run.

Yields:

  • (line)

Yield Parameters:

  • line (String)

    a line in the LOG_PATHNAME between `before(:suite)` and `after(:suite)`

Yield Returns:

  • (void)


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 156

def self.each_suite_line
  in_suite = false

  LOG_PATHNAME.each_line do |line|
    if in_suite
      if line.start_with?('after(:suite)')
        break
      else
        yield line
      end
    else
      if line.start_with?('before(:suite)')
        in_suite = true
      end
    end
  end
end

.each_thread_line {|uuid, line| ... } ⇒ Object

Note:

Ensure LOG_PATHNAME exists before calling.

Yield each line for each Thread UUID gathered during the suite run.

Yields:

  • (uuid, line)

Yield Parameters:

  • uuid (String)

    the UUID of thread thread

  • line (String)

    a line in the `caller` for the given `uuid`

Yield Returns:

  • (void)


182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 182

def self.each_thread_line
  in_thread_caller = false
  uuid = nil

  each_suite_line do |line|
    if in_thread_caller
      if line.start_with?('END Thread.new caller')
        in_thread_caller = false
        next
      else
        yield uuid, line
      end
    else
      match = line.match(UUID_REGEXP)

      if match
        in_thread_caller = true
        uuid = match[:uuid]
      end
    end
  end
end

.non_debugger_thread_listObject

Returns:



221
222
223
224
225
226
227
228
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 221

def self.non_debugger_thread_list
  Thread.list.reject { |thread|
    # don't do `is_a? Debugger::DebugThread` because it requires Debugger::DebugThread to be loaded, which it
    # won't when not debugging.
    thread.class.name == 'Debugger::DebugThread' ||
      thread.class.name == 'Debase::DebugThread'
  }
end