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)`

ENV['REMOTE_DB'] ? 4 : 3
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>})

191
192
193
194
195
196
197
198
199
200
201
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 191

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.


31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 31

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]

              # 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 status is #{thread.status.inspect} " \
                             "and was started here:\n"

              error_lines.concat(caller)
            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


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 115

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)

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 139

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)

165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 165

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:


204
205
206
207
208
209
210
211
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 204

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