Commit d52b5766 authored by Tim Peters's avatar Tim Peters

Fixing a rare BTree conflict resolution error.

+ Transaction T1 deletes some of the keys in bucket B,
  but not all of the keys.

+ Transaction T2 deletes (exactly) the keys in B that
  aren't deleted by T1.

The version of B each transaction sees is then non-empty,
but conflict resolution creates an empty bucket.  However,
conflict resolution doesn't have enough info to unlink an
empty bucket from its containing BTree correctly.

The result is that an empty bucket is left in the BTree,
which violates a BTree invariant.  The most probable
symptom is a segfault, when later & unrelated code tries
to access this bucket:  an empty bucket has NULL
pointers where the vectors of keys and values should be,
and accessing code tries to dereference the NULL pointers.

I don't know that this error has been seen in real life.
It was provoked by a randomized multithreaded simulation
program that was trying to provoke errors.  This error was
provoked frequently by that program; no other kinds of
errors have come out of it.
parent b3667600
......@@ -12,7 +12,7 @@
****************************************************************************/
#define MERGETEMPLATE_C "$Id: MergeTemplate.c,v 1.15 2002/06/25 22:02:27 tim_one Exp $\n"
#define MERGETEMPLATE_C "$Id: MergeTemplate.c,v 1.16 2003/01/17 17:20:49 tim_one Exp $\n"
/****************************************************************************
Set operations
......@@ -253,6 +253,15 @@ bucket_merge(Bucket *s1, Bucket *s2, Bucket *s3)
if (i3.next(&i3) < 0) goto err;
}
/* If the output bucket is empty, conflict resolution doesn't have
* enough info to unlink it from its containing BTree correctly.
*/
if (r->len == 0)
{
merge_error(-1, -1, -1, 10);
goto err;
}
finiSetIteration(&i1);
finiSetIteration(&i2);
finiSetIteration(&i3);
......
......@@ -191,7 +191,7 @@ class MappingBase(Base):
b1.clear()
bm.clear()
test_merge(base, b1, b2, bm, 'empty one and not other')
test_merge(base, b1, b2, bm, 'empty one and not other', should_fail=1)
def testFailMergeInsert(self):
base, b1, b2, bm, e1, e2, items = self._setupConflict()
......@@ -289,7 +289,7 @@ class SetTests(Base):
b1.clear()
bm.clear()
test_merge(base, b1, b2, bm, 'empty one and not other')
test_merge(base, b1, b2, bm, 'empty one and not other', should_fail=1)
def testFailMergeInsert(self):
base, b1, b2, bm, e1, e2, items = self._setupConflict()
......@@ -586,22 +586,12 @@ class NastyConfict(Base, TestCase):
self.assertEqual(state[0][1], 60)
self.assertEqual(state[0][3], 120)
# Conflict resolution empties bucket1 entirely.
# XXX This is broken: it doesn't raise ConflictError now.
### XXX The ConflictError imported at the top of this module isn't
### XXX the ConflictError that gets raised here.
##from zodb.interfaces import ConflictError
##self.assertRaises(ConflictError, get_transaction().commit)
##get_transaction().abort() # horrible things happen w/o this
# XXX Instead it creates an insane BTree (with an empty bucket
# XXX still linked in. Remove the remaining lines and uncomment
# XXX the lines above when this is fixed.
# XXX AssertionError: Bucket length < 1
get_transaction().commit()
self.assertRaises(AssertionError, b._check)
# Conflict resolution empties bucket1 entirely. This used to
# create an "insane" BTree (a legit BTree cannot contain an empty
# bucket -- it contains NULL pointers the BTree code doesn't
# expect, and segfaults result).
self.assertRaises(ConflictError, get_transaction().commit)
get_transaction().abort() # horrible things happen w/o this
def testEmptyBucketNoConflict(self):
# Tests that a plain empty bucket (on input) is not viewed as a
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment