Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/metasploit/framework/spec/threads/suite.rb
Views: 11788
require 'pathname'12# @note needs to use explicit nesting. so this file can be loaded directly without loading 'metasploit/framework' which3# allows for faster loading of rake tasks.4module Metasploit5module Framework6module Spec7module Threads8module Suite9#10# CONSTANTS11#1213# Number of allowed threads when threads are counted in `after(:suite)` or `before(:suite)`14#15# Known threads:16# 1. Main Ruby thread17# 2. Active Record connection pool thread18# 3. Framework thread manager, a monitor thread for removing dead threads19# https://github.com/rapid7/metasploit-framework/blame/04e8752b9b74cbaad7cb0ea6129c90e3172580a2/lib/msf/core/thread_manager.rb#L66-L8920# 4. Ruby's Timeout library thread, an automatically created monitor thread when using `Thread.timeout(1) { }`21# https://github.com/ruby/timeout/blob/bd25f4b138b86ef076e6d9d7374b159fffe5e4e9/lib/timeout.rb#L129-L13722# 5. REMOTE_DB thread, if enabled23#24# Intermittent threads that are non-deterministically left behind, which should be fixed in the future:25# 1. metadata cache hydration26# https://github.com/rapid7/metasploit-framework/blob/115946cd06faccac654e956e8ba9cf72ff328201/lib/msf/core/modules/metadata/cache.rb#L150-L15327# 2. session manager28# https://github.com/rapid7/metasploit-framework/blob/115946cd06faccac654e956e8ba9cf72ff328201/lib/msf/core/session_manager.rb#L153-L16829#30EXPECTED_THREAD_COUNT_AROUND_SUITE = ENV['REMOTE_DB'] ? 7 : 63132# `caller` for all Thread.new calls33LOG_PATHNAME = Pathname.new('log/metasploit/framework/spec/threads/suite.log')34# Regular expression for extracting the UUID out of {LOG_PATHNAME} for each Thread.new caller block35UUID_REGEXP = /BEGIN Thread.new caller \((?<uuid>.*)\)/36# Name of thread local variable that Thread UUID is stored37UUID_THREAD_LOCAL_VARIABLE = "metasploit/framework/spec/threads/logger/uuid"3839#40# Module Methods41#4243# Configures `before(:suite)` and `after(:suite)` callback to detect thread leaks.44#45# @return [void]46def self.configure!47unless @configured48RSpec.configure do |config|49config.before(:suite) do50thread_count = Metasploit::Framework::Spec::Threads::Suite.non_debugger_thread_list.count5152# check with if first so that error message can be constructed lazily53if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE54# LOG_PATHNAME may not exist if suite run without `rake spec`55if LOG_PATHNAME.exist?56log = LOG_PATHNAME.read()57else58log "Run `rake spec` to log where Thread.new is called."59end6061raise RuntimeError,62"#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when " \63"only #{EXPECTED_THREAD_COUNT_AROUND_SUITE} " \64"#{'thread'.pluralize(EXPECTED_THREAD_COUNT_AROUND_SUITE)} expected before suite runs:\n" \65"#{log}"66end6768LOG_PATHNAME.parent.mkpath6970LOG_PATHNAME.open('a') do |f|71# separator so after(:suite) can differentiate between threads created before(:suite) and during the72# suites73f.puts 'before(:suite)'74end75end7677config.after(:suite) do78LOG_PATHNAME.parent.mkpath7980LOG_PATHNAME.open('a') do |f|81# separator so that a flip flop can be used when reading the file below. Also useful if it turns82# out any threads are being created after this callback, which could be the case if another83# after(:suite) accidentally created threads by creating an Msf::Simple::Framework instance.84f.puts 'after(:suite)'85end8687thread_list = Metasploit::Framework::Spec::Threads::Suite.non_debugger_thread_list88thread_count = thread_list.count8990if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE91error_lines = []9293if LOG_PATHNAME.exist?94caller_by_thread_uuid = Metasploit::Framework::Spec::Threads::Suite.caller_by_thread_uuid9596thread_list.each do |thread|97thread_uuid = thread[Metasploit::Framework::Spec::Threads::Suite::UUID_THREAD_LOCAL_VARIABLE]98thread_name = thread[:tm_name]99100# unmanaged thread, such as the main VM thread101unless thread_uuid102next103end104105caller = caller_by_thread_uuid[thread_uuid]106107error_lines << "Thread #{thread_uuid}'s (name=#{thread_name} status is #{thread.status.inspect} " \108"and was started here:\n"109error_lines.concat(caller)110error_lines << "The thread backtrace was:\n#{thread.backtrace ? thread.backtrace.join("\n") : 'nil (no backtrace)'}\n"111end112else113error_lines << "Run `rake spec` to log where Thread.new is called."114end115116raise RuntimeError,117"#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when only " \118"#{EXPECTED_THREAD_COUNT_AROUND_SUITE} " \119"#{'thread'.pluralize(EXPECTED_THREAD_COUNT_AROUND_SUITE)} expected after suite runs:\n" \120"#{error_lines.join}"121end122end123end124125@configured = true126end127128@configured129end130131def self.define_task132Rake::Task.define_task('metasploit:framework:spec:threads:suite') do133if Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.exist?134Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.delete135end136137parent_pathname = Pathname.new(__FILE__).parent138threads_logger_pathname = parent_pathname.join('logger')139load_pathname = parent_pathname.parent.parent.parent.parent.expand_path140141# Must append to RUBYOPT or Rubymine debugger will not work142ENV['RUBYOPT'] = "#{ENV['RUBYOPT']} -I#{load_pathname} -r#{threads_logger_pathname}"143end144145Rake::Task.define_task(spec: 'metasploit:framework:spec:threads:suite')146end147148# @note Ensure {LOG_PATHNAME} exists before calling.149#150# Yields each line of {LOG_PATHNAME} that happened during the suite run.151#152# @yield [line]153# @yieldparam line [String] a line in the {LOG_PATHNAME} between `before(:suite)` and `after(:suite)`154# @yieldreturn [void]155def self.each_suite_line156in_suite = false157158LOG_PATHNAME.each_line do |line|159if in_suite160if line.start_with?('after(:suite)')161break162else163yield line164end165else166if line.start_with?('before(:suite)')167in_suite = true168end169end170end171end172173# @note Ensure {LOG_PATHNAME} exists before calling.174#175# Yield each line for each Thread UUID gathered during the suite run.176#177# @yield [uuid, line]178# @yieldparam uuid [String] the UUID of thread thread179# @yieldparam line [String] a line in the `caller` for the given `uuid`180# @yieldreturn [void]181def self.each_thread_line182in_thread_caller = false183uuid = nil184185each_suite_line do |line|186if in_thread_caller187if line.start_with?('END Thread.new caller')188in_thread_caller = false189next190else191yield uuid, line192end193else194match = line.match(UUID_REGEXP)195196if match197in_thread_caller = true198uuid = match[:uuid]199end200end201end202end203204# The `caller` for each Thread UUID.205#206# @return [Hash{String => Array<String>}]207def self.caller_by_thread_uuid208lines_by_thread_uuid = Hash.new { |hash, uuid|209hash[uuid] = []210}211212each_thread_line do |uuid, line|213lines_by_thread_uuid[uuid] << line214end215216lines_by_thread_uuid217end218219# @return220def self.non_debugger_thread_list221Thread.list.reject { |thread|222# don't do `is_a? Debugger::DebugThread` because it requires Debugger::DebugThread to be loaded, which it223# won't when not debugging.224thread.class.name == 'Debugger::DebugThread' ||225thread.class.name == 'Debase::DebugThread'226}227end228end229end230end231end232end233234235