Skip to content

Commit 8a4fb07

Browse files
committed
Merge branch 'bug/read-module-content-errno-enoent' into rapid7
Really [Closes rapid7#1025]
2 parents 04a80e0 + aaa5a3c commit 8a4fb07

21 files changed

+844
-97
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
.yardoc
77
# Mac OS X files
88
.DS_Store
9+
# simplecov coverage data
10+
coverage
911
data/meterpreter/ext_server_pivot.dll
1012
data/meterpreter/ext_server_pivot.x64.dll
1113
doc

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ end
2424
group :test do
2525
# testing framework
2626
gem 'rspec'
27+
# code coverage for tests
28+
# any version newer than 0.5.4 gives an Encoding error when trying to read the source files.
29+
gem 'simplecov', '0.5.4', :require => false
2730
end

Gemfile.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ GEM
4545
rspec-expectations (2.11.3)
4646
diff-lcs (~> 1.1.3)
4747
rspec-mocks (2.11.3)
48+
simplecov (0.5.4)
49+
multi_json (~> 1.0.3)
50+
simplecov-html (~> 0.5.3)
51+
simplecov-html (0.5.3)
4852
slop (3.3.3)
4953
tzinfo (0.3.33)
5054
yard (0.8.2.1)
@@ -60,4 +64,5 @@ DEPENDENCIES
6064
rake
6165
redcarpet
6266
rspec
67+
simplecov (= 0.5.4)
6368
yard

lib/msf/core/module_manager/loading.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,4 @@ def load_modules(path, options={})
109109

110110
count_by_type
111111
end
112-
end
112+
end

lib/msf/core/modules/error.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Base error class for all error under {Msf::Modules}
2+
class Msf::Modules::Error < StandardError
3+
def initialize(attributes={})
4+
@module_path = attributes[:module_path]
5+
@module_reference_name = attributes[:module_reference_name]
6+
7+
message_parts = []
8+
message_parts << "Failed to load module"
9+
10+
if module_reference_name or module_path
11+
clause_parts = []
12+
13+
if module_reference_name
14+
clause_parts << module_reference_name
15+
end
16+
17+
if module_path
18+
clause_parts << "from #{module_path}"
19+
end
20+
21+
clause = clause_parts.join(' ')
22+
message_parts << "(#{clause})"
23+
end
24+
25+
causal_message = attributes[:causal_message]
26+
27+
if causal_message
28+
message_parts << "due to #{causal_message}"
29+
end
30+
31+
message = message_parts.join(' ')
32+
33+
super(message)
34+
end
35+
36+
attr_reader :module_reference_name
37+
attr_reader :module_path
38+
end

lib/msf/core/modules/loader/base.rb

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44
require 'msf/core/modules/loader'
55
require 'msf/core/modules/namespace'
6+
require 'msf/core/modules/metasploit_class_compatibility_error'
67
require 'msf/core/modules/version_compatibility_error'
78

89
# Responsible for loading modules for {Msf::ModuleManager}.
@@ -117,12 +118,17 @@ def load_module(parent_path, type, module_reference_name, options={})
117118

118119
metasploit_class = nil
119120

121+
module_content = read_module_content(parent_path, type, module_reference_name)
122+
123+
if module_content.empty?
124+
# read_module_content is responsible for calling {#load_error}, so just return here.
125+
return false
126+
end
127+
120128
loaded = namespace_module_transaction(type + "/" + module_reference_name, :reload => reload) { |namespace_module|
121129
# set the parent_path so that the module can be reloaded with #load_module
122130
namespace_module.parent_path = parent_path
123131

124-
module_content = read_module_content(parent_path, type, module_reference_name)
125-
126132
begin
127133
namespace_module.module_eval_with_lexical_scope(module_content, module_path)
128134
# handle interrupts as pass-throughs unlike other Exceptions so users can bail with Ctrl+C
@@ -133,45 +139,33 @@ def load_module(parent_path, type, module_reference_name, options={})
133139
begin
134140
namespace_module.version_compatible!(module_path, module_reference_name)
135141
rescue Msf::Modules::VersionCompatibilityError => version_compatibility_error
136-
error_message = "Failed to load module (#{module_path}) due to error and #{version_compatibility_error}"
142+
load_error(module_path, version_compatibility_error)
137143
else
138-
error_message = "#{error.class} #{error}"
144+
load_error(module_path, error)
139145
end
140146

141-
# record the error message without the backtrace for the console
142-
module_manager.module_load_error_by_path[module_path] = error_message
143-
144-
error_message_with_backtrace = "#{error_message}:\n#{error.backtrace.join("\n")}"
145-
elog(error_message_with_backtrace)
146-
147147
return false
148148
end
149149

150150
begin
151151
namespace_module.version_compatible!(module_path, module_reference_name)
152152
rescue Msf::Modules::VersionCompatibilityError => version_compatibility_error
153-
error_message = version_compatibility_error.to_s
154-
155-
elog(error_message)
156-
module_manager.module_load_error_by_path[module_path] = error_message
153+
load_error(module_path, version_compatibility_error)
157154

158155
return false
159156
end
160157

161-
metasploit_class = namespace_module.metasploit_class
162-
163-
unless metasploit_class
164-
error_message = "Missing Metasploit class constant"
165-
166-
elog(error_message)
167-
module_manager.module_load_error_by_path[module_path] = error_message
158+
begin
159+
metasploit_class = namespace_module.metasploit_class!(module_path, module_reference_name)
160+
rescue Msf::Modules::MetasploitClassCompatibilityError => error
161+
load_error(module_path, error)
168162

169-
return false
163+
return false
170164
end
171165

172166
unless usable?(metasploit_class)
173167
ilog(
174-
"Skipping module #{module_reference_name} under #{parent_path} because is_usable returned false.",
168+
"Skipping module (#{module_reference_name} from #{module_path}) because is_usable returned false.",
175169
'core',
176170
LEV_1
177171
)
@@ -409,6 +403,29 @@ def each_module_reference_name(path)
409403
raise ::NotImplementedError
410404
end
411405

406+
# Records the load error to {Msf::ModuleManager::Loading#module_load_error_by_path} and the log.
407+
#
408+
# @param [String] module_path Path to the module as returned by {#module_path}.
409+
# @param [Exception, #class, #to_s, #backtrace] error the error that cause the module not to load.
410+
# @return [void]
411+
#
412+
# @see #module_path
413+
def load_error(module_path, error)
414+
# module_load_error_by_path does not get the backtrace because the value is echoed to the msfconsole where
415+
# backtraces should not appear.
416+
module_manager.module_load_error_by_path[module_path] = "#{error.class} #{error}"
417+
418+
log_lines = []
419+
log_lines << "#{module_path} failed to load due to the following error:"
420+
log_lines << error.class.to_s
421+
log_lines << error.to_s
422+
log_lines << "Call stack:"
423+
log_lines += error.backtrace
424+
425+
log_message = log_lines.join("\n")
426+
elog(log_message)
427+
end
428+
412429
# @return [Msf::ModuleManager] The module manager for which this loader is loading modules.
413430
attr_reader :module_manager
414431

lib/msf/core/modules/loader/directory.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,17 @@ def read_module_content(parent_path, type, module_reference_name)
7575

7676
module_content = ''
7777

78-
# force to read in binary mode so Pro modules won't be truncated on Windows
79-
File.open(full_path, 'rb') do |f|
80-
# Pass the size of the file as it leads to faster reads due to fewer buffer resizes. Greatest effect on Windows.
81-
# @see http://www.ruby-forum.com/topic/209005
82-
# @see https://github.com/ruby/ruby/blob/ruby_1_8_7/io.c#L1205
83-
# @see https://github.com/ruby/ruby/blob/ruby_1_9_3/io.c#L2038
84-
module_content = f.read(f.stat.size)
78+
begin
79+
# force to read in binary mode so Pro modules won't be truncated on Windows
80+
File.open(full_path, 'rb') do |f|
81+
# Pass the size of the file as it leads to faster reads due to fewer buffer resizes. Greatest effect on Windows.
82+
# @see http://www.ruby-forum.com/topic/209005
83+
# @see https://github.com/ruby/ruby/blob/ruby_1_8_7/io.c#L1205
84+
# @see https://github.com/ruby/ruby/blob/ruby_1_9_3/io.c#L2038
85+
module_content = f.read(f.stat.size)
86+
end
87+
rescue Errno::ENOENT => error
88+
load_error(full_path, error)
8589
end
8690

8791
module_content
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require 'msf/core/modules/error'
2+
3+
# Error raised by {Msf::Modules::Namespace#metasploit_class!} if it cannot the namespace_module does not have a constant
4+
# with {Msf::Framework::Major} or lower as a number after 'Metasploit', which indicates a compatible Msf::Module.
5+
class Msf::Modules::MetasploitClassCompatibilityError < Msf::Modules::Error
6+
def initialize(attributes={})
7+
super_attributes = {
8+
:causal_message => 'Missing compatible Metasploit<major_version> class constant',
9+
}.merge(attributes)
10+
11+
super(super_attributes)
12+
end
13+
end

lib/msf/core/modules/namespace.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ module Msf::Modules::Namespace
1010
# @return [nil] if such as class is not defined.
1111
def metasploit_class
1212
metasploit_class = nil
13-
# don't search ancestors for the metasploit_class
14-
#inherit = false
1513

1614
::Msf::Framework::Major.downto(1) do |major|
1715
# Since we really only care about the deepest namespace, we don't
@@ -29,6 +27,19 @@ def metasploit_class
2927
metasploit_class
3028
end
3129

30+
def metasploit_class!(module_path, module_reference_name)
31+
metasploit_class = self.metasploit_class
32+
33+
unless metasploit_class
34+
raise Msf::Modules::MetasploitClassCompatibilityError.new(
35+
:module_path => module_path,
36+
:module_reference_name => module_reference_name
37+
)
38+
end
39+
40+
metasploit_class
41+
end
42+
3243
# Raises an error unless {Msf::Framework::VersionCore} and {Msf::Framework::VersionAPI} meet the minimum required
3344
# versions defined in RequiredVersions in the module content.
3445
#

lib/msf/core/modules/version_compatibility_error.rb

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,43 @@
1+
require 'msf/core/modules/error'
2+
13
# Error raised by {Msf::Modules::Namespace#version_compatible!} on {Msf::Modules::Loader::Base#create_namespace_module}
24
# if the API or Core version does not meet the minimum requirements defined in the RequiredVersions constant in the
35
# {Msf::Modules::Loader::Base#read_module_content module content}.
4-
class Msf::Modules::VersionCompatibilityError < StandardError
6+
class Msf::Modules::VersionCompatibilityError < Msf::Modules::Error
57
# @param [Hash{Symbol => Float}] attributes
68
# @option attributes [Float] :minimum_api_version The minimum {Msf::Framework::VersionAPI} as defined in
79
# RequiredVersions.
810
# @option attributes [Float] :minimum_core_version The minimum {Msf::Framework::VersionCore} as defined in
911
# RequiredVersions.
1012
def initialize(attributes={})
11-
@module_path = attributes[:module_path]
12-
@module_reference_name = attributes[:module_reference_name]
1313
@minimum_api_version = attributes[:minimum_api_version]
1414
@minimum_core_version = attributes[:minimum_core_version]
1515

16-
super("Failed to reload module (#{module_reference_name} from #{module_path}) due to version check " \
17-
"(requires API:#{minimum_api_version} Core:#{minimum_core_version})")
16+
message_parts = []
17+
message_parts << 'version check'
18+
19+
if minimum_api_version or minimum_core_version
20+
clause_parts = []
21+
22+
if minimum_api_version
23+
clause_parts << "API >= #{minimum_api_version}"
24+
end
25+
26+
if minimum_core_version
27+
clause_parts << "Core >= #{minimum_core_version}"
28+
end
29+
30+
clause = clause_parts.join(' and ')
31+
message_parts << "(requires #{clause})"
32+
end
33+
34+
causal_message = message_parts.join(' ')
35+
36+
super_attributes = {
37+
:causal_message => causal_message
38+
}.merge(attributes)
39+
40+
super(super_attributes)
1841
end
1942

2043
# @return [Float] The minimum value of {Msf::Framework::VersionAPI} for the module to be compatible.

0 commit comments

Comments
 (0)