Source

features/packaging / Lib / test / test_mutants.py

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() whether 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.
        mutate = 0   # disable mutation until key deleted
        i = random.randrange(len(keys))
        key = keys[i]
        del target[key]
        del keys[i]
        mutate = 1

# 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).
        # XXX This is no longer effective.
        ##self.hashcode = random.randrange(1000000000)

    def __hash__(self):
        return 42
        return self.hashcode

    def __eq__(self, other):
        maybe_mutate()   # The point of the test.
        return self.i == other.i

    def __ne__(self, other):
        raise RuntimeError("I didn't expect some kind of Spanish inquisition!")

    __lt__ = __le__ = __gt__ = __ge__ = __ne__

    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 range(numentries):
        d[Horrid(random.choice(candidates))] = \
            Horrid(random.choice(candidates))
    return list(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), end=' ')
    while dict1 and len(dict1) == len(dict2):
        if verbose:
            print(".", end=' ')
        c = 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 range(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(Parent().__dict__, file=f)
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(file=f)

        return repr("machiavelli")

    def __hash__(self):
        return 0

dict[Machiavelli()] = Machiavelli()

print(str(dict), file=f)
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(dict[Machiavelli3(2)], file=f)
    except KeyError:
        pass
finally:
    f.close()
    os.unlink(TESTFN)

del dict
del dict1, dict2, dict1keys, dict2keys