Skip to content

Commit 259b2fa

Browse files
committed
Fix unsigned error value casting
When casting to an unsigned type from a python 2 `int`, we currently cast using `(unsigned long long) PyLong_AsUnsignedLong(src.ptr())`. If the Python cast fails, it returns (unsigned long) -1, but then we cast this to `unsigned long long`, which means we get 4294967295, but because that isn't equal to `(unsigned long long) -1`, we don't detect the failure. This commit moves the unsigned casting into a `detail::as_unsigned` function which, upon error, casts -1 to the final type, and otherwise casts the return value to the final type to avoid the problematic double cast when an error occurs. The error most commonly shows up wherever `long` is 32-bits (e.g. under both 32- and 64-bit Windows, and under 32-bit linux) when passing a negative value to a bound function taking an `unsigned long`. Fixes #929. The added tests also trigger a latent segfault under PyPy: when casting to an integer smaller than `long` (e.g. casting to a `uint32_t` on a 64-bit `long` architecture) we check both for a Python error and also that the resulting intermediate value will fit in the final type. If there is no conversion error, but we get a value that would overflow, we end up calling `PyErr_ExceptionMatches()` illegally: that call is only allowed when there is a current exception. Under PyPy, this segfaults the test suite. It doesn't appear to segfault under CPython, but the documentation suggests that it *could* do so. The fix is to only check for the exception match if we actually got an error.
1 parent 30f6c3b commit 259b2fa

File tree

5 files changed

+87
-33
lines changed

5 files changed

+87
-33
lines changed

include/pybind11/cast.h

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -929,31 +929,27 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
929929
py_value = (py_type) PyFloat_AsDouble(src.ptr());
930930
else
931931
return false;
932-
} else if (sizeof(T) <= sizeof(long)) {
933-
if (PyFloat_Check(src.ptr()))
934-
return false;
935-
if (std::is_signed<T>::value)
936-
py_value = (py_type) PyLong_AsLong(src.ptr());
937-
else
938-
py_value = (py_type) PyLong_AsUnsignedLong(src.ptr());
939-
} else {
940-
if (PyFloat_Check(src.ptr()))
941-
return false;
942-
if (std::is_signed<T>::value)
943-
py_value = (py_type) PYBIND11_LONG_AS_LONGLONG(src.ptr());
944-
else
945-
py_value = (py_type) PYBIND11_LONG_AS_UNSIGNED_LONGLONG(src.ptr());
932+
} else if (PyFloat_Check(src.ptr())) {
933+
return false;
934+
} else if (std::is_unsigned<py_type>::value) {
935+
py_value = as_unsigned<py_type>(src.ptr());
936+
} else { // signed integer:
937+
py_value = sizeof(T) <= sizeof(long)
938+
? (py_type) PyLong_AsLong(src.ptr())
939+
: (py_type) PYBIND11_LONG_AS_LONGLONG(src.ptr());
946940
}
947941

948-
if ((py_value == (py_type) -1 && PyErr_Occurred()) ||
949-
(std::is_integral<T>::value && sizeof(py_type) != sizeof(T) &&
950-
(py_value < (py_type) std::numeric_limits<T>::min() ||
951-
py_value > (py_type) std::numeric_limits<T>::max()))) {
942+
bool py_err = py_value == (py_type) -1 && PyErr_Occurred();
943+
if (py_err || (std::is_integral<T>::value && sizeof(py_type) != sizeof(T) &&
944+
(py_value < (py_type) std::numeric_limits<T>::min() ||
945+
py_value > (py_type) std::numeric_limits<T>::max()))) {
946+
bool type_error = py_err && PyErr_ExceptionMatches(
952947
#if PY_VERSION_HEX < 0x03000000 && !defined(PYPY_VERSION)
953-
bool type_error = PyErr_ExceptionMatches(PyExc_SystemError);
948+
PyExc_SystemError
954949
#else
955-
bool type_error = PyErr_ExceptionMatches(PyExc_TypeError);
950+
PyExc_TypeError
956951
#endif
952+
);
957953
PyErr_Clear();
958954
if (type_error && convert && PyNumber_Check(src.ptr())) {
959955
auto tmp = reinterpret_borrow<object>(std::is_floating_point<T>::value

include/pybind11/common.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@
147147
#define PYBIND11_BYTES_SIZE PyBytes_Size
148148
#define PYBIND11_LONG_CHECK(o) PyLong_Check(o)
149149
#define PYBIND11_LONG_AS_LONGLONG(o) PyLong_AsLongLong(o)
150-
#define PYBIND11_LONG_AS_UNSIGNED_LONGLONG(o) PyLong_AsUnsignedLongLong(o)
151150
#define PYBIND11_BYTES_NAME "bytes"
152151
#define PYBIND11_STRING_NAME "str"
153152
#define PYBIND11_SLICE_OBJECT PyObject
@@ -167,7 +166,6 @@
167166
#define PYBIND11_BYTES_SIZE PyString_Size
168167
#define PYBIND11_LONG_CHECK(o) (PyInt_Check(o) || PyLong_Check(o))
169168
#define PYBIND11_LONG_AS_LONGLONG(o) (PyInt_Check(o) ? (long long) PyLong_AsLong(o) : PyLong_AsLongLong(o))
170-
#define PYBIND11_LONG_AS_UNSIGNED_LONGLONG(o) (PyInt_Check(o) ? (unsigned long long) PyLong_AsUnsignedLong(o) : PyLong_AsUnsignedLongLong(o))
171169
#define PYBIND11_BYTES_NAME "str"
172170
#define PYBIND11_STRING_NAME "unicode"
173171
#define PYBIND11_SLICE_OBJECT PySliceObject

include/pybind11/pytypes.h

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,28 @@ class bool_ : public object {
931931
}
932932
};
933933

934+
NAMESPACE_BEGIN(detail)
935+
// Converts a value to the given unsigned type. If an error occurs, you get back (Unsigned) -1;
936+
// otherwise you get back the unsigned long or unsigned long long value cast to (Unsigned).
937+
// (The distinction is critically important when casting a returned -1 error value to some other
938+
// unsigned type: (A)-1 != (B)-1 when A and B are unsigned types of different sizes).
939+
template <typename Unsigned>
940+
Unsigned as_unsigned(PyObject *o) {
941+
if (sizeof(Unsigned) <= sizeof(unsigned long)
942+
#if PY_VERSION_HEX < 0x03000000
943+
|| PyInt_Check(o)
944+
#endif
945+
) {
946+
unsigned long v = PyLong_AsUnsignedLong(o);
947+
return v == (unsigned long) -1 && PyErr_Occurred() ? (Unsigned) -1 : (Unsigned) v;
948+
}
949+
else {
950+
unsigned long long v = PyLong_AsUnsignedLongLong(o);
951+
return v == (unsigned long long) -1 && PyErr_Occurred() ? (Unsigned) -1 : (Unsigned) v;
952+
}
953+
}
954+
NAMESPACE_END(detail)
955+
934956
class int_ : public object {
935957
public:
936958
PYBIND11_OBJECT_CVT(int_, object, PYBIND11_LONG_CHECK, PyNumber_Long)
@@ -956,17 +978,11 @@ class int_ : public object {
956978
template <typename T,
957979
detail::enable_if_t<std::is_integral<T>::value, int> = 0>
958980
operator T() const {
959-
if (sizeof(T) <= sizeof(long)) {
960-
if (std::is_signed<T>::value)
961-
return (T) PyLong_AsLong(m_ptr);
962-
else
963-
return (T) PyLong_AsUnsignedLong(m_ptr);
964-
} else {
965-
if (std::is_signed<T>::value)
966-
return (T) PYBIND11_LONG_AS_LONGLONG(m_ptr);
967-
else
968-
return (T) PYBIND11_LONG_AS_UNSIGNED_LONGLONG(m_ptr);
969-
}
981+
return std::is_unsigned<T>::value
982+
? detail::as_unsigned<T>(m_ptr)
983+
: sizeof(T) <= sizeof(long)
984+
? (T) PyLong_AsLong(m_ptr)
985+
: (T) PYBIND11_LONG_AS_LONGLONG(m_ptr);
970986
}
971987
};
972988

tests/test_builtin_casters.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ TEST_SUBMODULE(builtin_casters, m) {
7272
m.def("string_view32_return", []() { return std::u32string_view(U"utf32 secret \U0001f382"); });
7373
#endif
7474

75+
// test_integer_casting
76+
m.def("i32_str", [](std::int32_t v) { return std::to_string(v); });
77+
m.def("u32_str", [](std::uint32_t v) { return std::to_string(v); });
78+
m.def("i64_str", [](std::int64_t v) { return std::to_string(v); });
79+
m.def("u64_str", [](std::uint64_t v) { return std::to_string(v); });
80+
7581
// test_tuple
7682
m.def("pair_passthrough", [](std::pair<bool, std::string> input) {
7783
return std::make_pair(input.second, input.first);

tests/test_builtin_casters.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,44 @@ def test_string_view(capture):
143143
"""
144144

145145

146+
def test_integer_casting():
147+
"""Issue #929 - out-of-range integer values shouldn't be accepted"""
148+
import sys
149+
assert m.i32_str(-1) == "-1"
150+
assert m.i64_str(-1) == "-1"
151+
assert m.i32_str(2000000000) == "2000000000"
152+
assert m.u32_str(2000000000) == "2000000000"
153+
if sys.version_info < (3,):
154+
assert m.i32_str(long(-1)) == "-1"
155+
assert m.i64_str(long(-1)) == "-1"
156+
assert m.i64_str(long(-999999999999)) == "-999999999999"
157+
assert m.u64_str(long(999999999999)) == "999999999999"
158+
else:
159+
assert m.i64_str(-999999999999) == "-999999999999"
160+
assert m.u64_str(999999999999) == "999999999999"
161+
162+
with pytest.raises(TypeError) as excinfo:
163+
m.u32_str(-1)
164+
assert "incompatible function arguments" in str(excinfo.value)
165+
with pytest.raises(TypeError) as excinfo:
166+
m.u64_str(-1)
167+
assert "incompatible function arguments" in str(excinfo.value)
168+
with pytest.raises(TypeError) as excinfo:
169+
m.i32_str(-3000000000)
170+
assert "incompatible function arguments" in str(excinfo.value)
171+
with pytest.raises(TypeError) as excinfo:
172+
m.i32_str(3000000000)
173+
assert "incompatible function arguments" in str(excinfo.value)
174+
175+
if sys.version_info < (3,):
176+
with pytest.raises(TypeError) as excinfo:
177+
m.u32_str(long(-1))
178+
assert "incompatible function arguments" in str(excinfo.value)
179+
with pytest.raises(TypeError) as excinfo:
180+
m.u64_str(long(-1))
181+
assert "incompatible function arguments" in str(excinfo.value)
182+
183+
146184
def test_tuple(doc):
147185
"""std::pair <-> tuple & std::tuple <-> tuple"""
148186
assert m.pair_passthrough((True, "test")) == ("test", True)

0 commit comments

Comments
 (0)