Skip to content

Commit 0ebff0e

Browse files
committed
Do not assume a set element has been remembered for references
Code run while a later element is dumped (its __buffer__() method, an audit hook) can remove an element from the set and add another one, so that the size does not change. The removed element is still written, but it is then referenced only by the writer, so w_ref() does not enter it in the hashtable, and w_tie_set_items() dereferenced a NULL entry. Such an element cannot be written again, so it is skipped. Reported by Inada Naoki in the review of GH-159197.
1 parent 154f14c commit 0ebff0e

2 files changed

Lines changed: 63 additions & 4 deletions

File tree

‎Lib/test/test_marshal.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,59 @@ def visit(x, where):
13801380
dumps[name, int(version)],
13811381
(name, version))
13821382

1383+
def test_nan_in_set_changed(self):
1384+
# An element which is removed from the set, and another one added,
1385+
# while a later element is dumped (here by its __buffer__() method)
1386+
# is still written, since it was seen; but it is then referenced
1387+
# only by the writer. A NaN in that situation must not be assumed
1388+
# to have been remembered for later references.
1389+
class Item:
1390+
def __init__(self, action, h):
1391+
self.action = action
1392+
self.h = h
1393+
def __hash__(self):
1394+
return self.h
1395+
def __buffer__(self, flags):
1396+
# Called twice: to dump the element, then to write it.
1397+
action, self.action = self.action, None
1398+
if action is not None:
1399+
action()
1400+
return memoryview(b'abc')
1401+
1402+
def layout(nans, extra):
1403+
# A hash for the item and a replacement element such that the
1404+
# NaNs are iterated before the item, and the replacement lands
1405+
# in a slot which has been iterated already: the writer sees
1406+
# as many elements as the set has, and does not notice.
1407+
for h in range(128):
1408+
for k in range(128, 256):
1409+
s = set(nans) | set(extra)
1410+
item = Item(None, h)
1411+
s.add(item)
1412+
seen = []
1413+
for x in s:
1414+
seen.append(x)
1415+
if x is item:
1416+
s.discard(nans[0])
1417+
s.add(k)
1418+
if (len(seen) == len(s) and seen[-1] is item
1419+
and all(any(n is x for x in seen) for n in nans)):
1420+
return h, k
1421+
self.skipTest('no suitable set layout found')
1422+
1423+
for version in range(marshal.version + 1):
1424+
with self.subTest(version=version):
1425+
nans = [float('nan') for i in range(3)]
1426+
h, k = layout(nans, [1.5])
1427+
s = set(nans) | {1.5}
1428+
def remove_one():
1429+
s.discard(nans.pop(0))
1430+
s.add(k)
1431+
s.add(Item(remove_one, h))
1432+
new = marshal.loads(marshal.dumps(s, version))
1433+
self.assertEqual(sorted(map(repr, new)),
1434+
['1.5', "b'abc'", 'nan', 'nan', 'nan'])
1435+
13831436
def check_same_structure(self, new, sample, shared):
13841437
# Both object graphs have the same values and, if shared is true,
13851438
# share the same objects. Elements of sets are matched in any

‎Python/marshal.c‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -728,10 +728,16 @@ w_tie_set_items(w_set_item *items, Py_ssize_t count, size_t first, WFILE *p)
728728
for (Py_ssize_t i = 0; i < count; i++) {
729729
_Py_hashtable_entry_t *entry = _Py_hashtable_get_entry(
730730
p->hashtable, items[i].value);
731-
assert(entry != NULL);
732-
assert(((uintptr_t)entry->value >> W_ENTRY_SHIFT) - first
733-
< (size_t)count);
734-
assert(!((uintptr_t)entry->value & (W_ENTRY_ACTIVE | W_ENTRY_TIED)));
731+
if (entry == NULL) {
732+
/* The element was removed from the set (and another one added)
733+
by code run while a later element was dumped, so it was only
734+
referenced by items when it was written: w_ref() has not
735+
remembered it, and it cannot be written again. */
736+
continue;
737+
}
738+
uintptr_t w = (uintptr_t)entry->value;
739+
assert(!(w & (W_ENTRY_ACTIVE | W_ENTRY_TIED)));
740+
assert((w >> W_ENTRY_SHIFT) - first < (size_t)count);
735741
entry->value = (void *)value;
736742
}
737743
}

0 commit comments

Comments
 (0)