# Copyright 2013 Free Software Foundation, Inc. # # This is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see # . import gcc import gccutils import sys want_raii_info = False logging = False show_cfg = False def log(msg, indent=0): global logging if logging: sys.stderr.write('%s%s\n' % (' ' * indent, msg)) sys.stderr.flush() def is_cleanup_type(return_type): if not isinstance(return_type, gcc.PointerType): return False if not isinstance(return_type.dereference, gcc.RecordType): return False if str(return_type.dereference.name) == 'cleanup': return True return False def is_constructor(decl): "Return True if the function DECL is a cleanup constructor; False otherwise" return is_cleanup_type(decl.type.type) and (not decl.name or str(decl.name) != 'make_final_cleanup') destructor_names = set(['do_cleanups', 'discard_cleanups']) def is_destructor(decl): return decl.name in destructor_names # This list is just much too long... we should probably have an # attribute instead. special_names = set(['do_final_cleanups', 'discard_final_cleanups', 'save_cleanups', 'save_final_cleanups', 'restore_cleanups', 'restore_final_cleanups', 'exceptions_state_mc_init', 'make_my_cleanup2', 'make_final_cleanup', 'all_cleanups', 'save_my_cleanups', 'quit_target']) def needs_special_treatment(decl): return decl.name in special_names # Sometimes we need a new placeholder object that isn't the same as # anything else. class Dummy(object): def __init__(self, location): self.location = location # A wrapper for a cleanup which has been assigned to a variable. # This holds the variable and the location. class Cleanup(object): def __init__(self, var, location): self.var = var self.location = location # A class representing a master cleanup. This holds a stack of # cleanup objects and supports a merging operation. class MasterCleanup(object): # Create a new MasterCleanup object. OTHER, if given, is a # MasterCleanup object to copy. def __init__(self, other = None): # 'cleanups' is a list of cleanups. Each element is either a # Dummy, for an anonymous cleanup, or a Cleanup, for a cleanup # which was assigned to a variable. if other is None: self.cleanups = [] self.aliases = {} else: self.cleanups = other.cleanups[:] self.aliases = dict(other.aliases) def compare_vars(self, definition, argument): if definition == argument: return True if argument in self.aliases: argument = self.aliases[argument] if definition in self.aliases: definition = self.aliases[definition] return definition == argument def note_assignment(self, lhs, rhs): log('noting assignment %s = %s' % (lhs, rhs), 4) self.aliases[lhs] = rhs # Merge with another MasterCleanup. # Returns True if this resulted in a change to our state. def merge(self, other): # We do explicit iteration like this so we can easily # update the list after the loop. counter = -1 found_named = False for counter in range(len(self.cleanups) - 1, -1, -1): var = self.cleanups[counter] log('merge checking %s' % var, 4) # Only interested in named cleanups. if isinstance(var, Dummy): log('=> merge dummy', 5) continue # Now see if VAR is found in OTHER. if other._find_var(var.var) >= 0: log ('=> merge found', 5) break log('=>merge not found', 5) found_named = True if found_named and counter < len(self.cleanups) - 1: log ('merging to %d' % counter, 4) if counter < 0: self.cleanups = [] else: self.cleanups = self.cleanups[0:counter] return True # If SELF is empty but OTHER has some cleanups, then consider # that a change as well. if len(self.cleanups) == 0 and len(other.cleanups) > 0: log('merging non-empty other', 4) self.cleanups = other.cleanups[:] return True return False # Push a new constructor onto our stack. LHS is the # left-hand-side of the GimpleCall statement. It may be None, # meaning that this constructor's value wasn't used. def push(self, location, lhs): if lhs is None: obj = Dummy(location) else: obj = Cleanup(lhs, location) log('pushing %s' % lhs, 4) idx = self._find_var(lhs) if idx >= 0: gcc.permerror(location, 'reassigning to known cleanup') gcc.inform(self.cleanups[idx].location, 'previous assignment is here') self.cleanups.append(obj) # A helper for merge and pop that finds BACK_TO in self.cleanups, # and returns the index, or -1 if not found. def _find_var(self, back_to): for i in range(len(self.cleanups) - 1, -1, -1): if isinstance(self.cleanups[i], Dummy): continue if self.compare_vars(self.cleanups[i].var, back_to): return i return -1 # Pop constructors until we find one matching BACK_TO. # This is invoked when we see a do_cleanups call. def pop(self, location, back_to): log('pop:', 4) i = self._find_var(back_to) if i >= 0: self.cleanups = self.cleanups[0:i] else: gcc.permerror(location, 'destructor call with unknown argument') # Check whether ARG is the current master cleanup. Return True if # all is well. def verify(self, location, arg): log('verify %s' % arg, 4) return (len(self.cleanups) > 0 and not isinstance(self.cleanups[0], Dummy) and self.compare_vars(self.cleanups[0].var, arg)) # Check whether SELF is empty. def isempty(self): log('isempty: len = %d' % len(self.cleanups), 4) return len(self.cleanups) == 0 # Emit informational warnings about the cleanup stack. def inform(self): for item in reversed(self.cleanups): gcc.inform(item.location, 'leaked cleanup') class CleanupChecker: def __init__(self, fun): self.fun = fun self.seen_edges = set() self.bad_returns = set() # This maps BB indices to a list of master cleanups for the # BB. self.master_cleanups = {} # Pick a reasonable location for the basic block BB. def guess_bb_location(self, bb): if isinstance(bb.gimple, list): for stmt in bb.gimple: if stmt.loc: return stmt.loc return self.fun.end # Compute the master cleanup list for BB. # Modifies MASTER_CLEANUP in place. def compute_master(self, bb, bb_from, master_cleanup): if not isinstance(bb.gimple, list): return curloc = self.fun.end for stmt in bb.gimple: if stmt.loc: curloc = stmt.loc if isinstance(stmt, gcc.GimpleCall) and stmt.fndecl: if is_constructor(stmt.fndecl): log('saw constructor %s in bb=%d' % (str(stmt.fndecl), bb.index), 2) self.cleanup_aware = True master_cleanup.push(curloc, stmt.lhs) elif is_destructor(stmt.fndecl): if str(stmt.fndecl.name) != 'do_cleanups': self.only_do_cleanups_seen = False log('saw destructor %s in bb=%d, bb_from=%d, argument=%s' % (str(stmt.fndecl.name), bb.index, bb_from, str(stmt.args[0])), 2) master_cleanup.pop(curloc, stmt.args[0]) elif needs_special_treatment(stmt.fndecl): pass # gcc.permerror(curloc, 'function needs special treatment') elif isinstance(stmt, gcc.GimpleAssign): if isinstance(stmt.lhs, gcc.VarDecl) and isinstance(stmt.rhs[0], gcc.VarDecl): master_cleanup.note_assignment(stmt.lhs, stmt.rhs[0]) elif isinstance(stmt, gcc.GimpleReturn): if self.is_constructor: if not master_cleanup.verify(curloc, stmt.retval): gcc.permerror(curloc, 'constructor does not return master cleanup') elif not self.is_special_constructor: if not master_cleanup.isempty(): if curloc not in self.bad_returns: gcc.permerror(curloc, 'cleanup stack is not empty at return') self.bad_returns.add(curloc) master_cleanup.inform() # Traverse a basic block, updating the master cleanup information # and propagating to other blocks. def traverse_bbs(self, edge, bb, bb_from, entry_master): log('traverse_bbs %d from %d' % (bb.index, bb_from), 1) # Propagate the entry MasterCleanup though this block. master_cleanup = MasterCleanup(entry_master) self.compute_master(bb, bb_from, master_cleanup) modified = False if bb.index in self.master_cleanups: # Merge the newly-computed MasterCleanup into the one we # have already computed. If this resulted in a # significant change, then we need to re-propagate. modified = self.master_cleanups[bb.index].merge(master_cleanup) else: self.master_cleanups[bb.index] = master_cleanup modified = True # EDGE is None for the entry BB. if edge is not None: # If merging cleanups caused a change, check to see if we # have a bad loop. if edge in self.seen_edges: # This error doesn't really help. # if modified: # gcc.permerror(self.guess_bb_location(bb), # 'invalid cleanup use in loop') return self.seen_edges.add(edge) if not modified: return # Now propagate to successor nodes. for edge in bb.succs: self.traverse_bbs(edge, edge.dest, bb.index, master_cleanup) def check_cleanups(self): if not self.fun.cfg or not self.fun.decl: return 'ignored' if is_destructor(self.fun.decl): return 'destructor' if needs_special_treatment(self.fun.decl): return 'special' self.is_constructor = is_constructor(self.fun.decl) self.is_special_constructor = not self.is_constructor and str(self.fun.decl.name).find('with_cleanup') > -1 # Yuck. if str(self.fun.decl.name) == 'gdb_xml_create_parser_and_cleanup_1': self.is_special_constructor = True if self.is_special_constructor: gcc.inform(self.fun.start, 'function %s is a special constructor' % (self.fun.decl.name)) # If we only see do_cleanups calls, and this function is not # itself a constructor, then we can convert it easily to RAII. self.only_do_cleanups_seen = not self.is_constructor # If we ever call a constructor, then we are "cleanup-aware". self.cleanup_aware = False entry_bb = self.fun.cfg.entry master_cleanup = MasterCleanup() self.traverse_bbs(None, entry_bb, -1, master_cleanup) if want_raii_info and self.only_do_cleanups_seen and self.cleanup_aware: gcc.inform(self.fun.decl.location, 'function %s could be converted to RAII' % (self.fun.decl.name)) if self.is_constructor: return 'constructor' return 'OK' class CheckerPass(gcc.GimplePass): def execute(self, fun): if fun.decl: log("Starting " + fun.decl.name) if show_cfg: dot = gccutils.cfg_to_dot(fun.cfg, fun.decl.name) gccutils.invoke_dot(dot, name=fun.decl.name) checker = CleanupChecker(fun) what = checker.check_cleanups() if fun.decl: log(fun.decl.name + ': ' + what, 2) ps = CheckerPass(name = 'check-cleanups') # We need the cfg, but we want a relatively high-level Gimple. ps.register_after('cfg')