diff --git a/README.md b/README.md index 8905074c..26924322 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,10 @@ method in that module which should provide it with the information necessary to compile the current file. (You can also provide a path to a global `.ycm_extra_conf.py` file, which will be used as a fallback. See the Options section for more details.) +To prevent the execution of malicious code from a file you didn't write +YCM will ask once per module if it is safe to be loaded. +(This can be disabled. See the Options section.) + This system was designed this way so that the user can perform any arbitrary sequence of operations to produce a list of compilation flags YCM should hand @@ -651,6 +655,16 @@ Default: `''` let g:ycm_global_ycm_extra_conf = '' +### The `g:ycm_confirm_extra_conf` option + +When this option is set to `1` YCM will ask once per '.ycm_extra_conf.py' file +if it is safe to be loaded. This is to prevent execution of malicious code +from a '.ycm_extra_conf.py' file you didn't write. + +Default: `1` + + let g:ycm_confirm_extra_conf = 1 + ### The `g:ycm_semantic_triggers` option This option controls the character-based triggers for the various semantic diff --git a/plugin/youcompleteme.vim b/plugin/youcompleteme.vim index fd4c5bfa..1624aabd 100644 --- a/plugin/youcompleteme.vim +++ b/plugin/youcompleteme.vim @@ -103,6 +103,9 @@ let g:ycm_key_detailed_diagnostics = let g:ycm_global_ycm_extra_conf = \ get( g:, 'ycm_global_ycm_extra_conf', '' ) +let g:ycm_confirm_extra_conf = + \ get( g:, 'ycm_confirm_extra_conf', 1 ) + let g:ycm_semantic_triggers = \ get( g:, 'ycm_semantic_triggers', { \ 'c' : ['->', '.'], diff --git a/python/completers/cpp/clang_completer.py b/python/completers/cpp/clang_completer.py index 6f3a37ae..456f80e3 100644 --- a/python/completers/cpp/clang_completer.py +++ b/python/completers/cpp/clang_completer.py @@ -206,7 +206,8 @@ class ClangCompleter( Completer ): def DebugInfo( self ): filename = vim.current.buffer.name flags = self.flags.FlagsForFile( filename ) or [] - return 'Flags for {0}:\n{1}'.format( filename, list( flags ) ) + source = self.flags.ModuleForFile( filename ) + return 'Flags for {0} loaded from {1}:\n{2}'.format( filename, source, list( flags ) ) # TODO: make these functions module-local diff --git a/python/completers/cpp/flags.py b/python/completers/cpp/flags.py index 646995d9..7bec7ee3 100644 --- a/python/completers/cpp/flags.py +++ b/python/completers/cpp/flags.py @@ -29,32 +29,49 @@ YCM_EXTRA_CONF_FILENAME = '.ycm_extra_conf.py' NO_EXTRA_CONF_FILENAME_MESSAGE = ('No {0} file detected, so no compile flags ' 'are available. Thus no semantic support for C/C++/ObjC/ObjC++. See the ' 'docs for details.').format( YCM_EXTRA_CONF_FILENAME ) +CONFIRM_CONF_FILE_MESSAGE = 'Found {0}. Load?' GLOBAL_YCM_EXTRA_CONF_FILE = os.path.expanduser( vimsupport.GetVariableValue( "g:ycm_global_ycm_extra_conf" ) ) class Flags( object ): + """Keeps track of the flags necessary to compile a file. + The flags are loaded from user-created python files + (hereafter referred to as 'modules') that contain + a method FlagsForFile( filename ).""" def __init__( self ): # It's caches all the way down... self.flags_for_file = {} - self.flags_module_for_file = {} - self.flags_module_for_flags_module_file = {} + self.module_for_file = {} + self.modules = FlagsModules() self.special_clang_flags = _SpecialClangIncludes() self.no_extra_conf_file_warning_posted = False + def ModuleForFile( self, filename ): + """This will try all files returned by _FlagsModuleSourceFilesForFile in + order and return the filename of the first module that was allowed to load. + If no module was found or allowed to load, None is returned.""" + + if not self.module_for_file.has_key( filename ): + for flags_module_file in _FlagsModuleSourceFilesForFile( filename ): + if self.modules.Load( flags_module_file ): + self.module_for_file[ filename ] = flags_module_file + break + + return self.module_for_file.setdefault( filename ) def FlagsForFile( self, filename ): try: return self.flags_for_file[ filename ] except KeyError: - flags_module = self._FlagsModuleForFile( filename ) - if not flags_module: + module_file = self.ModuleForFile( filename ) + if not module_file: if not self.no_extra_conf_file_warning_posted: vimsupport.PostVimMessage( NO_EXTRA_CONF_FILENAME_MESSAGE ) self.no_extra_conf_file_warning_posted = True return None - results = flags_module.FlagsForFile( filename ) + results = self.modules[ module_file ].FlagsForFile( filename ) if not results.get( 'flags_ready', True ): return None @@ -66,58 +83,95 @@ class Flags( object ): self.flags_for_file[ filename ] = sanitized_flags return sanitized_flags - - def _FlagsModuleForFile( self, filename ): - try: - return self.flags_module_for_file[ filename ] - except KeyError: - flags_module_file = _FlagsModuleSourceFileForFile( filename ) - if not flags_module_file: - return None - - try: - flags_module = self.flags_module_for_flags_module_file[ - flags_module_file ] - except KeyError: - sys.path.insert( 0, _DirectoryOfThisScript() ) - flags_module = imp.load_source( _RandomName(), flags_module_file ) - del sys.path[ 0 ] - - self.flags_module_for_flags_module_file[ - flags_module_file ] = flags_module - - self.flags_module_for_file[ filename ] = flags_module - return flags_module + def ReloadModule( self, module_file ): + """Reloads a module file cleaning the flags cache for all files + associated with that module. Returns False if reloading failed + (for example due to the model not being loaded in the first place).""" + module_file = os.path.abspath(module_file) + if self.modules.Reload( module_file ): + for filename, module in self.module_for_file.iteritems(): + if module == module_file: + del self.flags_for_file[ filename ] + return True + return False +class FlagsModules( object ): + """Keeps track of modules. + Modules are loaded on-demand and cached in self.modules for quick access.""" + def __init__( self ): + self.modules = {} -def _FlagsModuleSourceFileForFile( filename ): - """For a given filename, finds its nearest YCM_EXTRA_CONF_FILENAME file that - will compute the flags necessary to compile the file. If no - YCM_EXTRA_CONF_FILENAME file could be found, try to use - GLOBAL_YCM_EXTRA_CONF_FILE instead. If that also fails, return None. - Uses the global ycm_extra_conf file if one is set.""" + def Disable( self, module_file ): + """Disables the loading of a module for the current session.""" + self.modules[ module_file ] = None - ycm_conf_file = None - parent_folder = os.path.dirname( filename ) - old_parent_folder = '' + @staticmethod + def ShouldLoad( module_file ): + """Checks if a module is safe to be loaded. + By default this will ask the user for confirmation.""" + if module_file == GLOBAL_YCM_EXTRA_CONF_FILE: + return True + if ( vimsupport.GetBoolValue( 'g:ycm_confirm_extra_conf' ) and + not vimsupport.Confirm( + CONFIRM_CONF_FILE_MESSAGE.format( module_file ) ) ): + return False + return True - while True: - current_file = os.path.join( parent_folder, YCM_EXTRA_CONF_FILENAME ) - if os.path.exists( current_file ): - ycm_conf_file = current_file - break + def Load( self, module_file, force = False ): + """Load and return the module contained in a file. + Using force = True the module will be loaded regardless + of the criteria in ShouldLoad. + This will return None if the module was not allowed to be loaded.""" + if not force: + if self.modules.has_key( module_file ): + return self.modules[ module_file ] - old_parent_folder = parent_folder - parent_folder = os.path.dirname( parent_folder ) - if parent_folder is old_parent_folder: - break + if not self.ShouldLoad( module_file ): + return self.Disable( module_file ) - if ( not ycm_conf_file and GLOBAL_YCM_EXTRA_CONF_FILE and - os.path.exists( GLOBAL_YCM_EXTRA_CONF_FILE ) ): - ycm_conf_file = GLOBAL_YCM_EXTRA_CONF_FILE + sys.path.insert( 0, _DirectoryOfThisScript() ) + module = imp.load_source( _RandomName(), module_file ) + del sys.path[ 0 ] - return ycm_conf_file + self.modules[ module_file ] = module + return module + + def Reload( self, module_file ): + """Reloads the given module. If it has not been loaded yet does nothing. + Note that the module will not be subject to the loading criteria again.""" + if self.modules.get( module_file ): + return self.Load( module_file, force = True ) + + def __getitem__( self, key ): + return self.Load( key ) + + +def _FlagsModuleSourceFilesForFile( filename ): + """For a given filename, search all parent folders for YCM_EXTRA_CONF_FILENAME + files that will compute the flags necessary to compile the file. + If GLOBAL_YCM_EXTRA_CONF_FILE exists it is returned as a fallback.""" + for folder in _PathsToAllParentFolders( filename ): + candidate = os.path.join( folder, YCM_EXTRA_CONF_FILENAME ) + if os.path.exists( candidate ): + yield candidate + if ( GLOBAL_YCM_EXTRA_CONF_FILE + and os.path.exists( GLOBAL_YCM_EXTRA_CONF_FILE ) ): + yield GLOBAL_YCM_EXTRA_CONF_FILE + + +def _PathsToAllParentFolders( filename ): + """Build a list of all parent folders of a file. + The neares files will be returned first. + Example: _PathsToAllParentFolders( '/home/user/projects/test.c' ) + [ '/home/user/projects', '/home/user', '/home', '/' ]""" + parent_folders = os.path.abspath( + os.path.dirname( filename ) ).split( os.path.sep ) + if not parent_folders[0]: + parent_folders[0] = os.path.sep + parent_folders = [ os.path.join( *parent_folders[:i + 1] ) + for i in xrange( len( parent_folders ) ) ] + return reversed( parent_folders ) def _RandomName():