mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 21:51:50 +00:00 
			
		
		
		
	 453163d842
			
		
	
	
		453163d842
		
	
	
	
	
		
			
			be possible to provoke unbounded recursion now, but leaving that to someone else to provoke and repair. Bugfix candidate -- although this is getting harder to backstitch, and the cases it's protecting against are mondo contrived.
		
			
				
	
	
		
			285 lines
		
	
	
	
		
			8.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
	
		
			8.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from test_support import verbose, TESTFN
 | |
| import random
 | |
| import os
 | |
| 
 | |
| # From SF bug #422121:  Insecurities in dict comparison.
 | |
| 
 | |
| # Safety of code doing comparisons has been an historical Python weak spot.
 | |
| # The problem is that comparison of structures written in C *naturally*
 | |
| # wants to hold on to things like the size of the container, or "the
 | |
| # biggest" containee so far, across a traversal of the container; but
 | |
| # code to do containee comparisons can call back into Python and mutate
 | |
| # the container in arbitrary ways while the C loop is in midstream.  If the
 | |
| # C code isn't extremely paranoid about digging things out of memory on
 | |
| # each trip, and artificially boosting refcounts for the duration, anything
 | |
| # from infinite loops to OS crashes can result (yes, I use Windows <wink>).
 | |
| #
 | |
| # The other problem is that code designed to provoke a weakness is usually
 | |
| # white-box code, and so catches only the particular vulnerabilities the
 | |
| # author knew to protect against.  For example, Python's list.sort() code
 | |
| # went thru many iterations as one "new" vulnerability after another was
 | |
| # discovered.
 | |
| #
 | |
| # So the dict comparison test here uses a black-box approach instead,
 | |
| # generating dicts of various sizes at random, and performing random
 | |
| # mutations on them at random times.  This proved very effective,
 | |
| # triggering at least six distinct failure modes the first 20 times I
 | |
| # ran it.  Indeed, at the start, the driver never got beyond 6 iterations
 | |
| # before the test died.
 | |
| 
 | |
| # The dicts are global to make it easy to mutate tham from within functions.
 | |
| dict1 = {}
 | |
| dict2 = {}
 | |
| 
 | |
| # The current set of keys in dict1 and dict2.  These are materialized as
 | |
| # lists to make it easy to pick a dict key at random.
 | |
| dict1keys = []
 | |
| dict2keys = []
 | |
| 
 | |
| # Global flag telling maybe_mutate() wether to *consider* mutating.
 | |
| mutate = 0
 | |
| 
 | |
| # If global mutate is true, consider mutating a dict.  May or may not
 | |
| # mutate a dict even if mutate is true.  If it does decide to mutate a
 | |
| # dict, it picks one of {dict1, dict2} at random, and deletes a random
 | |
| # entry from it; or, more rarely, adds a random element.
 | |
| 
 | |
| def maybe_mutate():
 | |
|     global mutate
 | |
|     if not mutate:
 | |
|         return
 | |
|     if random.random() < 0.5:
 | |
|         return
 | |
| 
 | |
|     if random.random() < 0.5:
 | |
|         target, keys = dict1, dict1keys
 | |
|     else:
 | |
|         target, keys = dict2, dict2keys
 | |
| 
 | |
|     if random.random() < 0.2:
 | |
|         # Insert a new key.
 | |
|         mutate = 0   # disable mutation until key inserted
 | |
|         while 1:
 | |
|             newkey = Horrid(random.randrange(100))
 | |
|             if newkey not in target:
 | |
|                 break
 | |
|         target[newkey] = Horrid(random.randrange(100))
 | |
|         keys.append(newkey)
 | |
|         mutate = 1
 | |
| 
 | |
|     elif keys:
 | |
|         # Delete a key at random.
 | |
|         i = random.randrange(len(keys))
 | |
|         key = keys[i]
 | |
|         del target[key]
 | |
|         # CAUTION:  don't use keys.remove(key) here.  Or do <wink>.  The
 | |
|         # point is that .remove() would trigger more comparisons, and so
 | |
|         # also more calls to this routine.  We're mutating often enough
 | |
|         # without that.
 | |
|         del keys[i]
 | |
| 
 | |
| # A horrid class that triggers random mutations of dict1 and dict2 when
 | |
| # instances are compared.
 | |
| 
 | |
| class Horrid:
 | |
|     def __init__(self, i):
 | |
|         # Comparison outcomes are determined by the value of i.
 | |
|         self.i = i
 | |
| 
 | |
|         # An artificial hashcode is selected at random so that we don't
 | |
|         # have any systematic relationship between comparison outcomes
 | |
|         # (based on self.i and other.i) and relative position within the
 | |
|         # hash vector (based on hashcode).
 | |
|         self.hashcode = random.randrange(1000000000)
 | |
| 
 | |
|     def __hash__(self):
 | |
|         return self.hashcode
 | |
| 
 | |
|     def __cmp__(self, other):
 | |
|         maybe_mutate()   # The point of the test.
 | |
|         return cmp(self.i, other.i)
 | |
| 
 | |
|     def __repr__(self):
 | |
|         return "Horrid(%d)" % self.i
 | |
| 
 | |
| # Fill dict d with numentries (Horrid(i), Horrid(j)) key-value pairs,
 | |
| # where i and j are selected at random from the candidates list.
 | |
| # Return d.keys() after filling.
 | |
| 
 | |
| def fill_dict(d, candidates, numentries):
 | |
|     d.clear()
 | |
|     for i in xrange(numentries):
 | |
|         d[Horrid(random.choice(candidates))] = \
 | |
|             Horrid(random.choice(candidates))
 | |
|     return d.keys()
 | |
| 
 | |
| # Test one pair of randomly generated dicts, each with n entries.
 | |
| # Note that dict comparison is trivial if they don't have the same number
 | |
| # of entires (then the "shorter" dict is instantly considered to be the
 | |
| # smaller one, without even looking at the entries).
 | |
| 
 | |
| def test_one(n):
 | |
|     global mutate, dict1, dict2, dict1keys, dict2keys
 | |
| 
 | |
|     # Fill the dicts without mutating them.
 | |
|     mutate = 0
 | |
|     dict1keys = fill_dict(dict1, range(n), n)
 | |
|     dict2keys = fill_dict(dict2, range(n), n)
 | |
| 
 | |
|     # Enable mutation, then compare the dicts so long as they have the
 | |
|     # same size.
 | |
|     mutate = 1
 | |
|     if verbose:
 | |
|         print "trying w/ lengths", len(dict1), len(dict2),
 | |
|     while dict1 and len(dict1) == len(dict2):
 | |
|         if verbose:
 | |
|             print ".",
 | |
|         c = cmp(dict1, dict2)
 | |
|     if verbose:
 | |
|         print
 | |
| 
 | |
| # Run test_one n times.  At the start (before the bugs were fixed), 20
 | |
| # consecutive runs of this test each blew up on or before the sixth time
 | |
| # test_one was run.  So n doesn't have to be large to get an interesting
 | |
| # test.
 | |
| # OTOH, calling with large n is also interesting, to ensure that the fixed
 | |
| # code doesn't hold on to refcounts *too* long (in which case memory would
 | |
| # leak).
 | |
| 
 | |
| def test(n):
 | |
|     for i in xrange(n):
 | |
|         test_one(random.randrange(1, 100))
 | |
| 
 | |
| # See last comment block for clues about good values for n.
 | |
| test(100)
 | |
| 
 | |
| ##########################################################################
 | |
| # Another segfault bug, distilled by Michael Hudson from a c.l.py post.
 | |
| 
 | |
| class Child:
 | |
|     def __init__(self, parent):
 | |
|         self.__dict__['parent'] = parent
 | |
|     def __getattr__(self, attr):
 | |
|         self.parent.a = 1
 | |
|         self.parent.b = 1
 | |
|         self.parent.c = 1
 | |
|         self.parent.d = 1
 | |
|         self.parent.e = 1
 | |
|         self.parent.f = 1
 | |
|         self.parent.g = 1
 | |
|         self.parent.h = 1
 | |
|         self.parent.i = 1
 | |
|         return getattr(self.parent, attr)
 | |
| 
 | |
| class Parent:
 | |
|     def __init__(self):
 | |
|         self.a = Child(self)
 | |
| 
 | |
| # Hard to say what this will print!  May vary from time to time.  But
 | |
| # we're specifically trying to test the tp_print slot here, and this is
 | |
| # the clearest way to do it.  We print the result to a temp file so that
 | |
| # the expected-output file doesn't need to change.
 | |
| 
 | |
| f = open(TESTFN, "w")
 | |
| print >> f, Parent().__dict__
 | |
| f.close()
 | |
| os.unlink(TESTFN)
 | |
| 
 | |
| ##########################################################################
 | |
| # And another core-dumper from Michael Hudson.
 | |
| 
 | |
| dict = {}
 | |
| 
 | |
| # Force dict to malloc its table.
 | |
| for i in range(1, 10):
 | |
|     dict[i] = i
 | |
| 
 | |
| f = open(TESTFN, "w")
 | |
| 
 | |
| class Machiavelli:
 | |
|     def __repr__(self):
 | |
|         dict.clear()
 | |
| 
 | |
|         # Michael sez:  "doesn't crash without this.  don't know why."
 | |
|         # Tim sez:  "luck of the draw; crashes with or without for me."
 | |
|         print >> f
 | |
| 
 | |
|         return `"machiavelli"`
 | |
| 
 | |
|     def __hash__(self):
 | |
|         return 0
 | |
| 
 | |
| dict[Machiavelli()] = Machiavelli()
 | |
| 
 | |
| print >> f, str(dict)
 | |
| f.close()
 | |
| os.unlink(TESTFN)
 | |
| del f, dict
 | |
| 
 | |
| 
 | |
| ##########################################################################
 | |
| # And another core-dumper from Michael Hudson.
 | |
| 
 | |
| dict = {}
 | |
| 
 | |
| # let's force dict to malloc its table
 | |
| for i in range(1, 10):
 | |
|     dict[i] = i
 | |
| 
 | |
| class Machiavelli2:
 | |
|     def __eq__(self, other):
 | |
|         dict.clear()
 | |
|         return 1
 | |
| 
 | |
|     def __hash__(self):
 | |
|         return 0
 | |
| 
 | |
| dict[Machiavelli2()] = Machiavelli2()
 | |
| 
 | |
| try:
 | |
|     dict[Machiavelli2()]
 | |
| except KeyError:
 | |
|     pass
 | |
| 
 | |
| del dict
 | |
| 
 | |
| ##########################################################################
 | |
| # And another core-dumper from Michael Hudson.
 | |
| 
 | |
| dict = {}
 | |
| 
 | |
| # let's force dict to malloc its table
 | |
| for i in range(1, 10):
 | |
|     dict[i] = i
 | |
| 
 | |
| class Machiavelli3:
 | |
|     def __init__(self, id):
 | |
|         self.id = id
 | |
| 
 | |
|     def __eq__(self, other):
 | |
|         if self.id == other.id:
 | |
|             dict.clear()
 | |
|             return 1
 | |
|         else:
 | |
|             return 0
 | |
| 
 | |
|     def __repr__(self):
 | |
|         return "%s(%s)"%(self.__class__.__name__, self.id)
 | |
| 
 | |
|     def __hash__(self):
 | |
|         return 0
 | |
| 
 | |
| dict[Machiavelli3(1)] = Machiavelli3(0)
 | |
| dict[Machiavelli3(2)] = Machiavelli3(0)
 | |
| 
 | |
| f = open(TESTFN, "w")
 | |
| try:
 | |
|     try:
 | |
|         print >> f, dict[Machiavelli3(2)]
 | |
|     except KeyError:
 | |
|         pass
 | |
| finally:
 | |
|     f.close()
 | |
|     os.unlink(TESTFN)
 | |
| 
 | |
| del dict
 |