mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 03:44:55 +00:00 
			
		
		
		
	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
 |