Skip to content

Commit fe1af35

Browse files
committed
First pass at changes needed for module metadata caching
1 parent 43ddc66 commit fe1af35

File tree

15 files changed

+303
-340
lines changed

15 files changed

+303
-340
lines changed

db/modules_metadata_base.pstore

2.79 MB
Binary file not shown.

lib/metasploit/framework/spec/constants.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module Metasploit::Framework::Spec::Constants
2222
Error
2323
External
2424
Loader
25+
Metadata
2526
MetasploitClassCompatibilityError
2627
Namespace
2728
VersionCompatibilityError

lib/msf/core/framework.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,6 @@ def initialize(options={})
9191
events.add_general_subscriber(subscriber)
9292
events.add_db_subscriber(subscriber)
9393
events.add_ui_subscriber(subscriber)
94-
95-
Msf::Modules::Metadata::Cache.instance.init(self)
9694
end
9795

9896
def inspect

lib/msf/core/module/full_name.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ module ClassMethods
2121
#
2222

2323
def fullname
24-
type + '/' + (refname.nil? ? "" : refname)
24+
type + '/' + (refname.nil? ? '' : refname)
2525
end
2626

2727
def shortname

lib/msf/core/module_manager/cache.rb

Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def load_cached_module(type, reference_name)
9898
end
9999

100100
# @overload refresh_cache_from_module_files
101-
# Rebuilds database and in-memory cache for all modules.
101+
# Rebuilds module metadat store and in-memory cache for all modules.
102102
#
103103
# @return [void]
104104
# @overload refresh_cache_from_module_files(module_class_or_instance)
@@ -107,14 +107,21 @@ def load_cached_module(type, reference_name)
107107
# @param (see Msf::DBManager#update_module_details)
108108
# @return [void]
109109
def refresh_cache_from_module_files(module_class_or_instance = nil)
110-
if framework_migrated?
111-
if module_class_or_instance
112-
framework.db.update_module_details(module_class_or_instance)
113-
else
114-
framework.db.update_all_module_details
115-
end
116-
refresh_cache_from_database(self.module_paths)
110+
if module_class_or_instance
111+
Msf::Modules::Metadata::Cache.instance.refresh_metadata_instance(module_class_or_instance)
112+
else
113+
module_sets =
114+
[
115+
['exploit', @framework.exploits],
116+
['auxiliary', @framework.auxiliary],
117+
['post', @framework.post],
118+
['payload', @framework.payloads],
119+
['encoder', @framework.encoders],
120+
['nop', @framework.nops]
121+
]
122+
Msf::Modules::Metadata::Cache.instance.refresh_metadata(module_sets)
117123
end
124+
refresh_cache_from_database(self.module_paths)
118125
end
119126

120127
# Refreshes the in-memory cache from the database cache.
@@ -126,19 +133,11 @@ def refresh_cache_from_database(allowed_paths=[""])
126133

127134
protected
128135

129-
# Returns whether the framework migrations have been run already.
130-
#
131-
# @return [true] if migrations have been run
132-
# @return [false] otherwise
133-
def framework_migrated?
134-
framework.db && framework.db.migrated
135-
end
136-
137136
# @!attribute [rw] module_info_by_path
138-
# @return (see #module_info_by_path_from_database!)
137+
# @return (see #module_info_by_path_from_store!)
139138
attr_accessor :module_info_by_path
140139

141-
# Return a module info from Mdm::Module::Details in database.
140+
# Return a module info from Msf::Modules::Metadata::Obj.
142141
#
143142
# @note Also sets module_set(module_type)[module_reference_name] to Msf::SymbolicModule if it is not already set.
144143
#
@@ -148,41 +147,35 @@ def framework_migrated?
148147
def module_info_by_path_from_database!(allowed_paths=[""])
149148
self.module_info_by_path = {}
150149

151-
if framework_migrated?
152-
allowed_paths = allowed_paths.map{|x| x + "/"}
153-
154-
ActiveRecord::Base.connection_pool.with_connection do
155-
# TODO record module parent_path in Mdm::Module::Detail so it does not need to be derived from file.
156-
# Use find_each so Mdm::Module::Details are returned in batches, which will
157-
# handle the growing number of modules better than all.each.
158-
Mdm::Module::Detail.find_each do |module_detail|
159-
path = module_detail.file
160-
type = module_detail.mtype
161-
reference_name = module_detail.refname
162-
163-
# Skip cached modules that are not in our allowed load paths
164-
next if allowed_paths.select{|x| path.index(x) == 0}.empty?
165-
166-
# The load path is assumed to be the next level above the type directory
167-
type_dir = File.join('', Mdm::Module::Detail::DIRECTORY_BY_TYPE[type], '')
168-
parent_path = path.split(type_dir)[0..-2].join(type_dir) # TODO: rewrite
169-
170-
module_info_by_path[path] = {
171-
:reference_name => reference_name,
172-
:type => type,
173-
:parent_path => parent_path,
174-
:modification_time => module_detail.mtime
175-
}
176-
177-
typed_module_set = module_set(type)
178-
179-
# Don't want to trigger as {Msf::ModuleSet#create} so check for
180-
# key instead of using ||= which would call {Msf::ModuleSet#[]}
181-
# which would potentially call {Msf::ModuleSet#create}.
182-
unless typed_module_set.has_key? reference_name
183-
typed_module_set[reference_name] = Msf::SymbolicModule
184-
end
185-
end
150+
allowed_paths = allowed_paths.map{|x| x + "/"}
151+
152+
metadata = Msf::Modules::Metadata::Cache.instance.get_metadata
153+
metadata.each do |module_metadata|
154+
path = module_metadata.path
155+
type = module_metadata.type
156+
reference_name = module_metadata.ref_name
157+
158+
# Skip cached modules that are not in our allowed load paths
159+
next if allowed_paths.select{|x| path.index(x) == 0}.empty?
160+
161+
# The load path is assumed to be the next level above the type directory
162+
type_dir = File.join('', Mdm::Module::Detail::DIRECTORY_BY_TYPE[type], '')
163+
parent_path = path.split(type_dir)[0..-2].join(type_dir) # TODO: rewrite
164+
165+
module_info_by_path[path] = {
166+
:reference_name => reference_name,
167+
:type => type,
168+
:parent_path => parent_path,
169+
:modification_time => module_metadata.mod_time
170+
}
171+
172+
typed_module_set = module_set(type)
173+
174+
# Don't want to trigger as {Msf::ModuleSet#create} so check for
175+
# key instead of using ||= which would call {Msf::ModuleSet#[]}
176+
# which would potentially call {Msf::ModuleSet#create}.
177+
unless typed_module_set.has_key? reference_name
178+
typed_module_set[reference_name] = Msf::SymbolicModule
186179
end
187180
end
188181

lib/msf/core/module_set.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,6 @@ def add_module(klass, reference_name, info = {})
205205

206206
self[reference_name] = klass
207207

208-
Msf::Modules::Metadata::Cache.instance.cache_module_metadata(klass)
209-
210208
klass
211209
end
212210

lib/msf/core/modules/metadata.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# -*- coding: binary -*-
2+
3+
# Namespace for module metadata related data and operations
14
module Msf::Modules::Metadata
25

3-
end
6+
end
7+

lib/msf/core/modules/metadata/cache.rb

Lines changed: 64 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -2,156 +2,120 @@
22
require 'msf/events'
33
require 'rex/ui/text/output/stdio'
44
require 'msf/core/constants'
5+
require 'msf/core/modules/metadata'
56
require 'msf/core/modules/metadata/obj'
67
require 'msf/core/modules/metadata/search'
8+
require 'msf/core/modules/metadata/store'
79

810
#
911
# Core service class that provides storage of module metadata as well as operations on the metadata.
1012
# Note that operations on this metadata are included as separate modules.
1113
#
12-
# To prevent excessive startup times module definitions are not parsed for metadata until startup
13-
# is complete. Once startup is complete to prevent CPU spin loading is then done gradually and
14-
# only when an operation using the cache is called is CPU use maximized.
15-
#
1614
module Msf
1715
module Modules
1816
module Metadata
1917

2018
class Cache
2119
include Singleton
22-
include ::Msf::UiEventSubscriber
2320
include Msf::Modules::Metadata::Search
21+
include Msf::Modules::Metadata::Store
2422

2523
#
26-
# Init registers this class as a listener to be notified when the console is done loading,
27-
# this acts a hint to this class to start trickle loading the metadata
24+
# Refreshes cached module metadata as well as updating the store
2825
#
29-
def init(framework)
30-
register_ui_listener(framework)
31-
@framework = framework
26+
def refresh_metadata_instance(module_instance)
27+
refresh_metadata_instance_internal(module_instance)
28+
update_store
3229
end
3330

3431
#
35-
# Parses module metadata into an in memory cache.
36-
#
37-
# @param klass_or_instance - An instantiated or class instance of a module.
32+
# Returns the module data cache, but first ensures all the metadata is loaded
3833
#
39-
def cache_module_metadata(klass_or_instance)
40-
# Only use cache if db is not in use for now
41-
return if @framework.db.active
42-
43-
if klass_or_instance.is_a?(Class)
44-
if @module_load_complete
45-
add_module_metadata_from_class(klass_or_instance)
46-
else
47-
@module_definitions << klass_or_instance
48-
end
49-
else
50-
add_module_metadata_from_instance(klass_or_instance)
51-
end
34+
def get_metadata
35+
wait_for_load
36+
@module_metadata_cache.values
5237
end
5338

54-
#########
55-
protected
56-
#########
57-
5839
#
59-
# Notify the thread responsible for loading metadata that it can start loading.
40+
# Checks for modules loaded that are not a part of the cache and updates the underlying store
41+
# if there are changes.
6042
#
61-
def on_ui_start(rev)
62-
return if @module_load_complete
63-
@startup_called = true
43+
def refresh_metadata(module_sets)
44+
unchanged_module_references = get_unchanged_module_references
45+
has_changes = false
46+
module_sets.each do |mt|
47+
unchanged_reference_name_set = unchanged_module_references[mt[0]]
48+
49+
mt[1].keys.sort.each do |mn|
50+
next if unchanged_reference_name_set.include? mn
51+
module_instance = mt[1].create(mn)
52+
next if not module_instance
53+
begin
54+
refresh_metadata_instance_internal(module_instance)
55+
has_changes = true
56+
rescue Exception => e
57+
elog("Error updating module details for #{module_instance.fullname}: #{$!.class} #{$!} : #{e.message}")
58+
end
59+
end
60+
end
61+
62+
update_store if has_changes
6463
end
6564

6665
#
67-
# Returns the module data cache, but first ensures all the metadata is loaded
66+
# Returns a hash(type->set) which references modules that have not changed.
6867
#
69-
def get_module_metadata_cache
70-
wait_for_load
71-
return @module_metadata_cache
68+
def get_unchanged_module_references
69+
skip_reference_name_set_by_module_type = Hash.new { |hash, module_type|
70+
hash[module_type] = Set.new
71+
}
72+
73+
@module_metadata_cache.each_value do |module_metadata|
74+
75+
unless module_metadata.path and ::File.exist?(module_metadata.path)
76+
next
77+
end
78+
79+
if ::File.mtime(module_metadata.path).to_i != module_metadata.mod_time.to_i
80+
next
81+
end
82+
83+
skip_reference_name_set = skip_reference_name_set_by_module_type[module_metadata.type]
84+
skip_reference_name_set.add(module_metadata.ref_name)
85+
end
86+
87+
return skip_reference_name_set_by_module_type
7288
end
7389

7490
#######
7591
private
7692
#######
7793

78-
def register_ui_listener(framework)
79-
begin
80-
framework.events.add_ui_subscriber(self)
81-
rescue Exception => e
82-
elog('Unable to register metadata cache service as UI listener')
83-
end
84-
end
85-
8694
def wait_for_load
87-
if (!@module_load_complete)
88-
@trickle_load = false
89-
@console.print_warning('Waiting to finish parsing module metadata')
90-
@module_load_thread.join
91-
end
95+
@load_thread.join unless @store_loaded
9296
end
9397

94-
def add_module_metadata_from_class(module_class_definition)
95-
begin
96-
instance = module_class_definition.new
97-
add_module_metadata_from_instance(instance)
98-
rescue Exception => e
99-
elog("Error adding module metadata: #{e.message}")
100-
end
101-
end
102-
103-
def add_module_metadata_from_instance(module_instance)
104-
module_metadata = Obj.new(module_instance)
105-
@module_metadata_cache[get_cache_key(module_instance)] = module_metadata
98+
def refresh_metadata_instance_internal(module_instance)
99+
metadata_obj = Obj.new(module_instance)
100+
@module_metadata_cache[get_cache_key(module_instance)] = metadata_obj
106101
end
107102

108103
def get_cache_key(module_instance)
109104
key = ''
110105
key << (module_instance.type.nil? ? '' : module_instance.type)
111106
key << '_'
112-
key << module_instance.name
107+
key << module_instance.refname
113108
return key
114109
end
115110

116-
#
117-
# This method is used by the @module_load_thread
118-
#
119-
def load_module_definitions
120-
loop do
121-
if @startup_called
122-
break;
123-
end
124-
125-
sleep 0.3
126-
end
127-
128-
count = 0
129-
@module_definitions.each {|module_definition|
130-
add_module_metadata_from_class(module_definition)
131-
count = count + 1
132-
if (@trickle_load && count > @trickle_load_batch)
133-
sleep @trickle_load_interval
134-
count = 0
135-
end
136-
}
137-
138-
@module_load_complete = true
139-
GC.start(full_mark: true, immediate_sweep: true)
140-
end
141-
142111
def initialize
143-
@module_load_complete = false
144-
@startup_called = false;
145-
@trickle_load = true
146-
@trickle_load_batch = 200
147-
@trickle_load_interval = 0.5
148112
@module_metadata_cache = {}
149-
@module_definitions = []
150-
@module_load_thread = Thread.new {
151-
load_module_definitions
152-
}
153-
113+
@store_loaded = false
154114
@console = Rex::Ui::Text::Output::Stdio.new
115+
@load_thread = Thread.new {
116+
init_store
117+
@store_loaded = true
118+
}
155119
end
156120
end
157121

0 commit comments

Comments
 (0)