Skip to content

Commit 5c6ba1d

Browse files
committed
COMPAT/TEST test, fix for unsafe Vector.resize(), which allows refcheck=False
1 parent 50f8b9f commit 5c6ba1d

File tree

4 files changed

+71
-22
lines changed

4 files changed

+71
-22
lines changed

pandas/_libs/hashtable.pxd

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ cdef struct Int64VectorData:
5252
cdef class Int64Vector:
5353
cdef Int64VectorData *data
5454
cdef ndarray ao
55+
cdef bint external_view_exists
5556

5657
cdef resize(self)
5758
cpdef to_array(self)

pandas/_libs/hashtable.pyx

+15
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ cdef class Factorizer:
6464
>>> factorize(np.array([1,2,np.nan], dtype='O'), na_sentinel=20)
6565
array([ 0, 1, 20])
6666
"""
67+
if self.uniques.external_view_exists:
68+
uniques = ObjectVector()
69+
data = self.uniques.to_array()
70+
for v in data:
71+
uniques.append(v)
72+
self.uniques = uniques
6773
labels = self.table.get_labels(values, self.uniques,
6874
self.count, na_sentinel, check_null)
6975
mask = (labels == na_sentinel)
@@ -99,6 +105,15 @@ cdef class Int64Factorizer:
99105

100106
def factorize(self, int64_t[:] values, sort=False,
101107
na_sentinel=-1, check_null=True):
108+
"""
109+
Factorize values with nans replaced by na_sentinel
110+
>>> factorize(np.array([1,2,np.nan], dtype='O'), na_sentinel=20)
111+
array([ 0, 1, 20])
112+
"""
113+
if self.uniques.external_view_exists:
114+
uniques = Int64Vector()
115+
uniques.extend(self.uniques.to_array())
116+
self.uniques = uniques
102117
labels = self.table.get_labels(values, self.uniques,
103118
self.count, na_sentinel,
104119
check_null)

pandas/_libs/hashtable_class_helper.pxi.in

+31-6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ cdef class {{name}}Vector:
7171

7272
{{if dtype != 'int64'}}
7373
cdef:
74+
bint external_view_exists
7475
{{name}}VectorData *data
7576
ndarray ao
7677
{{endif}}
@@ -80,14 +81,15 @@ cdef class {{name}}Vector:
8081
sizeof({{name}}VectorData))
8182
if not self.data:
8283
raise MemoryError()
84+
self.external_view_exists = False
8385
self.data.n = 0
8486
self.data.m = _INIT_VEC_CAP
8587
self.ao = np.empty(self.data.m, dtype={{idtype}})
8688
self.data.data = <{{arg}}*> self.ao.data
8789

8890
cdef resize(self):
8991
self.data.m = max(self.data.m * 4, _INIT_VEC_CAP)
90-
self.ao.resize(self.data.m)
92+
self.ao.resize(self.data.m, refcheck=False)
9193
self.data.data = <{{arg}}*> self.ao.data
9294

9395
def __dealloc__(self):
@@ -99,13 +101,19 @@ cdef class {{name}}Vector:
99101
return self.data.n
100102

101103
cpdef to_array(self):
102-
self.ao.resize(self.data.n)
104+
if self.data.m != self.data.n:
105+
if self.external_view_exists:
106+
raise ValueError("should have raised on append(), m=%d n=%d" % (self.data.m, self.data.n))
107+
self.ao.resize(self.data.n, refcheck=False)
103108
self.data.m = self.data.n
109+
self.external_view_exists = True
104110
return self.ao
105111

106112
cdef inline void append(self, {{arg}} x):
107113

108114
if needs_resize(self.data):
115+
if self.external_view_exists:
116+
raise ValueError("external reference but Vector.resize() needed")
109117
self.resize()
110118

111119
append_data_{{dtype}}(self.data, x)
@@ -120,15 +128,19 @@ cdef class StringVector:
120128

121129
cdef:
122130
StringVectorData *data
131+
bint external_view_exists
123132

124133
def __cinit__(self):
125134
self.data = <StringVectorData *>PyMem_Malloc(
126135
sizeof(StringVectorData))
127136
if not self.data:
128137
raise MemoryError()
138+
self.external_view_exists = False
129139
self.data.n = 0
130140
self.data.m = _INIT_VEC_CAP
131141
self.data.data = <char **> malloc(self.data.m * sizeof(char *))
142+
if not self.data.data:
143+
raise MemoryError()
132144

133145
cdef resize(self):
134146
cdef:
@@ -138,9 +150,10 @@ cdef class StringVector:
138150
m = self.data.m
139151
self.data.m = max(self.data.m * 4, _INIT_VEC_CAP)
140152

141-
# TODO: can resize?
142153
orig_data = self.data.data
143154
self.data.data = <char **> malloc(self.data.m * sizeof(char *))
155+
if not self.data.data:
156+
raise MemoryError()
144157
for i in range(m):
145158
self.data.data[i] = orig_data[i]
146159

@@ -164,6 +177,7 @@ cdef class StringVector:
164177
for i in range(self.data.n):
165178
val = self.data.data[i]
166179
ao[i] = val
180+
self.external_view_exists = True
167181
self.data.m = self.data.n
168182
return ao
169183

@@ -181,8 +195,10 @@ cdef class ObjectVector:
181195
PyObject **data
182196
size_t n, m
183197
ndarray ao
198+
bint external_view_exists
184199

185200
def __cinit__(self):
201+
self.external_view_exists = False
186202
self.n = 0
187203
self.m = _INIT_VEC_CAP
188204
self.ao = np.empty(_INIT_VEC_CAP, dtype=object)
@@ -193,17 +209,23 @@ cdef class ObjectVector:
193209

194210
cdef inline append(self, object o):
195211
if self.n == self.m:
212+
if self.external_view_exists:
213+
raise ValueError("external reference but Vector.resize() needed")
196214
self.m = max(self.m * 2, _INIT_VEC_CAP)
197-
self.ao.resize(self.m)
215+
self.ao.resize(self.m, refcheck=False)
198216
self.data = <PyObject**> self.ao.data
199217

200218
Py_INCREF(o)
201219
self.data[self.n] = <PyObject*> o
202220
self.n += 1
203221

204222
def to_array(self):
205-
self.ao.resize(self.n)
206-
self.m = self.n
223+
if self.m != self.n:
224+
if self.external_view_exists:
225+
raise ValueError("should have raised on append()")
226+
self.ao.resize(self.n, refcheck=False)
227+
self.m = self.n
228+
self.external_view_exists = True
207229
return self.ao
208230

209231

@@ -362,6 +384,9 @@ cdef class {{name}}HashTable(HashTable):
362384

363385
if needs_resize(ud):
364386
with gil:
387+
if uniques.external_view_exists:
388+
raise ValueError("external reference to uniques held, "
389+
"but Vector.resize() needed")
365390
uniques.resize()
366391
append_data_{{dtype}}(ud, val)
367392
labels[i] = count

pandas/tests/test_algos.py

+24-16
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from pandas import compat
1616
from pandas._libs import (groupby as libgroupby, algos as libalgos,
17-
hashtable)
17+
hashtable as ht)
1818
from pandas._libs.hashtable import unique_label_indices
1919
from pandas.compat import lrange, range
2020
import pandas.core.algorithms as algos
@@ -259,7 +259,7 @@ def test_factorize_nan(self):
259259
# rizer.factorize should not raise an exception if na_sentinel indexes
260260
# outside of reverse_indexer
261261
key = np.array([1, 2, 1, np.nan], dtype='O')
262-
rizer = hashtable.Factorizer(len(key))
262+
rizer = ht.Factorizer(len(key))
263263
for na_sentinel in (-1, 20):
264264
ids = rizer.factorize(key, sort=True, na_sentinel=na_sentinel)
265265
expected = np.array([0, 1, 0, na_sentinel], dtype='int32')
@@ -1049,14 +1049,14 @@ class TestHashTable(object):
10491049

10501050
def test_lookup_nan(self):
10511051
xs = np.array([2.718, 3.14, np.nan, -7, 5, 2, 3])
1052-
m = hashtable.Float64HashTable()
1052+
m = ht.Float64HashTable()
10531053
m.map_locations(xs)
10541054
tm.assert_numpy_array_equal(m.lookup(xs), np.arange(len(xs),
10551055
dtype=np.int64))
10561056

10571057
def test_lookup_overflow(self):
10581058
xs = np.array([1, 2, 2**63], dtype=np.uint64)
1059-
m = hashtable.UInt64HashTable()
1059+
m = ht.UInt64HashTable()
10601060
m.map_locations(xs)
10611061
tm.assert_numpy_array_equal(m.lookup(xs), np.arange(len(xs),
10621062
dtype=np.int64))
@@ -1070,25 +1070,33 @@ def test_vector_resize(self):
10701070
# Test for memory errors after internal vector
10711071
# reallocations (pull request #7157)
10721072

1073-
def _test_vector_resize(htable, uniques, dtype, nvals):
1073+
def _test_vector_resize(htable, uniques, dtype, nvals, safely_resizes):
10741074
vals = np.array(np.random.randn(1000), dtype=dtype)
10751075
# get_labels appends to the vector
10761076
htable.get_labels(vals[:nvals], uniques, 0, -1)
1077-
# to_array resizes the vector
1078-
uniques.to_array()
1079-
htable.get_labels(vals, uniques, 0, -1)
1077+
# to_array may resize the vector
1078+
tmp = uniques.to_array()
1079+
oldshape = tmp.shape
1080+
if safely_resizes:
1081+
htable.get_labels(vals, uniques, 0, -1)
1082+
else:
1083+
with pytest.raises(ValueError) as excinfo:
1084+
htable.get_labels(vals, uniques, 0, -1)
1085+
assert str(excinfo.value).startswith('external reference')
1086+
uniques.to_array() # should not raise
1087+
assert tmp.shape == oldshape
10801088

10811089
test_cases = [
1082-
(hashtable.PyObjectHashTable, hashtable.ObjectVector, 'object'),
1083-
(hashtable.StringHashTable, hashtable.ObjectVector, 'object'),
1084-
(hashtable.Float64HashTable, hashtable.Float64Vector, 'float64'),
1085-
(hashtable.Int64HashTable, hashtable.Int64Vector, 'int64'),
1086-
(hashtable.UInt64HashTable, hashtable.UInt64Vector, 'uint64')]
1090+
(ht.PyObjectHashTable, ht.ObjectVector, 'object', False),
1091+
(ht.StringHashTable, ht.ObjectVector, 'object', True),
1092+
(ht.Float64HashTable, ht.Float64Vector, 'float64', False),
1093+
(ht.Int64HashTable, ht.Int64Vector, 'int64', False),
1094+
(ht.UInt64HashTable, ht.UInt64Vector, 'uint64', False)]
10871095

1088-
for (tbl, vect, dtype) in test_cases:
1096+
for (tbl, vect, dtype, safely_resizes) in test_cases:
10891097
# resizing to empty is a special case
1090-
_test_vector_resize(tbl(), vect(), dtype, 0)
1091-
_test_vector_resize(tbl(), vect(), dtype, 10)
1098+
_test_vector_resize(tbl(), vect(), dtype, 0, safely_resizes)
1099+
_test_vector_resize(tbl(), vect(), dtype, 10, safely_resizes)
10921100

10931101

10941102
def test_quantile():

0 commit comments

Comments
 (0)