Skip to content

Commit c36584e

Browse files
oremanjwjakob
authored andcommitted
Fix several sources of undefined behavior in type casters
1 parent 983d6c0 commit c36584e

23 files changed

+423
-96
lines changed

docs/changelog.rst

+38
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,44 @@ noteworthy:
195195
specify the owner explicitly. The previous default (``nb::handle()``)
196196
continues to be a valid argument.
197197

198+
* There have been some changes to the API for type casters in order to
199+
avoid undefined behavior in certain cases. (PR `#549
200+
<https://github.com/wjakob/nanobind/pull/549>`__).
201+
202+
* Type casters that implement custom cast operators must now define a
203+
member function template ``can_cast<T>()``, which returns false if
204+
``operator cast_t<T>()`` would raise an exception and true otherwise.
205+
``can_cast<T>()`` will be called only after a successful call to
206+
``from_python()``, and might not be called at all if the caller of
207+
``operator cast_t<T>()`` can cope with a raised exception.
208+
(Users of the ``NB_TYPE_CASTER()`` convenience macro need not worry
209+
about this; it produces cast operators that never raise exceptions,
210+
and therefore provides a ``can_cast<T>()`` that always returns true.)
211+
212+
* Many type casters for container types (``std::vector<T>``,
213+
``std::optional<T>``, etc) implement their ``from_python()`` methods
214+
by delegating to another, "inner" type caster (``T`` in these examples)
215+
that is allocated on the stack inside ``from_python()``. Container casters
216+
implemented in this way should make two changes in order to take advantage
217+
of the new safety features:
218+
219+
* Wrap your ``flags`` (received as an argument of the outer caster's
220+
``from_python`` method) in ``flags_for_local_caster<T>()`` before
221+
passing them to ``inner_caster.from_python()``. This allows nanobind
222+
to prevent some casts that would produce dangling pointers or references.
223+
224+
* If ``inner_caster.from_python()`` succeeds, then also verify
225+
``inner_caster.template can_cast<T>()`` before you execute
226+
``inner_caster.operator cast_t<T>()``. A failure of
227+
``can_cast()`` should be treated the same as a failure of
228+
``from_python()``. This avoids the possibility of an exception
229+
being raised through the noexcept ``load_python()`` method,
230+
which would crash the interpreter.
231+
232+
The previous ``cast_flags::none_disallowed`` flag has been removed;
233+
it existed to avoid one particular source of exceptions from a cast
234+
operator, but ``can_cast<T>()`` now handles that problem more generally.
235+
198236
* ABI version 14.
199237

200238
.. rubric:: Footnote

include/nanobind/eigen/dense.h

+3
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ struct type_caster<T, enable_if_t<is_eigen_xpr_v<T> &&
220220
using Caster = make_caster<Array>;
221221
static constexpr auto Name = Caster::Name;
222222
template <typename T_> using Cast = T;
223+
template <typename T_> static constexpr bool can_cast() { return true; }
223224

224225
/// Generating an expression template from a Python object is, of course, not possible
225226
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept = delete;
@@ -250,6 +251,7 @@ struct type_caster<Eigen::Map<T, Options, StrideType>,
250251
using NDArrayCaster = type_caster<NDArray>;
251252
static constexpr auto Name = NDArrayCaster::Name;
252253
template <typename T_> using Cast = Map;
254+
template <typename T_> static constexpr bool can_cast() { return true; }
253255

254256
NDArrayCaster caster;
255257

@@ -403,6 +405,7 @@ struct type_caster<Eigen::Ref<T, Options, StrideType>,
403405
const_name<MaybeConvert>(DMapCaster::Name, MapCaster::Name);
404406

405407
template <typename T_> using Cast = Ref;
408+
template <typename T_> static constexpr bool can_cast() { return true; }
406409

407410
MapCaster caster;
408411
struct Empty { };

include/nanobind/eigen/sparse.h

+2
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ struct type_caster<Eigen::Map<T>, enable_if_t<is_eigen_sparse_matrix_v<T>>> {
150150
using SparseMatrixCaster = type_caster<T>;
151151
static constexpr auto Name = SparseMatrixCaster::Name;
152152
template <typename T_> using Cast = Map;
153+
template <typename T_> static constexpr bool can_cast() { return true; }
153154

154155
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept = delete;
155156

@@ -165,6 +166,7 @@ struct type_caster<Eigen::Ref<T, Options>, enable_if_t<is_eigen_sparse_matrix_v<
165166
using MapCaster = make_caster<Map>;
166167
static constexpr auto Name = MapCaster::Name;
167168
template <typename T_> using Cast = Ref;
169+
template <typename T_> static constexpr bool can_cast() { return true; }
168170

169171
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept = delete;
170172

include/nanobind/nb_cast.h

+120-36
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Value = Value_; \
1212
static constexpr auto Name = descr; \
1313
template <typename T_> using Cast = movable_cast_t<T_>; \
14+
template <typename T_> static constexpr bool can_cast() { return true; } \
1415
template <typename T_, \
1516
enable_if_t<std::is_same_v<std::remove_cv_t<T_>, Value>> = 0> \
1617
static handle from_cpp(T_ *p, rv_policy policy, cleanup_list *list) { \
@@ -38,11 +39,11 @@ enum cast_flags : uint8_t {
3839
// Passed to the 'self' argument in a constructor call (__init__)
3940
construct = (1 << 1),
4041

41-
// Don't accept 'None' Python objects in the base class caster
42-
none_disallowed = (1 << 2),
43-
44-
// Indicates that this cast is performed by nb::cast or nb::try_cast
45-
manual = (1 << 3)
42+
// Indicates that this cast is performed by nb::cast or nb::try_cast.
43+
// This implies that objects added to the cleanup list may be
44+
// released immediately after the caster's final output value is
45+
// obtained, i.e., before it is used.
46+
manual = (1 << 2),
4647
};
4748

4849
/**
@@ -88,6 +89,51 @@ using precise_cast_t =
8889
std::conditional_t<std::is_rvalue_reference_v<T>,
8990
intrinsic_t<T> &&, intrinsic_t<T> &>>;
9091

92+
/// Many type casters delegate to another caster using the pattern:
93+
/// ~~~ .cc
94+
/// bool from_python(handle src, uint8_t flags, cleanup_list *cl) noexcept {
95+
/// SomeCaster c;
96+
/// if (!c.from_python(src, flags, cl)) return false;
97+
/// /* do something with */ c.operator T();
98+
/// return true;
99+
/// }
100+
/// ~~~
101+
/// This function adjusts the flags to avoid issues where the resulting T object
102+
/// refers into storage that will dangle after SomeCaster is destroyed, and
103+
/// causes a static assertion failure if that's not sufficient. Use it like:
104+
/// ~~~ .cc
105+
/// if (!c.from_python(src, flags_for_local_caster<T>(flags), cl))
106+
/// return false;
107+
/// ~~~
108+
/// where the template argument T is the type you plan to extract.
109+
template <typename T>
110+
NB_INLINE uint8_t flags_for_local_caster(uint8_t flags) noexcept {
111+
constexpr bool is_ref = std::is_pointer_v<T> || std::is_reference_v<T>;
112+
if constexpr (is_base_caster_v<make_caster<T>>) {
113+
if constexpr (is_ref) {
114+
/* References/pointers to a type produced by implicit conversions
115+
refer to storage owned by the cleanup_list. In a nb::cast() call,
116+
that storage will be released before the reference can be used;
117+
to prevent dangling, don't allow implicit conversions there. */
118+
if (flags & ((uint8_t) cast_flags::manual))
119+
flags &= ~((uint8_t) cast_flags::convert);
120+
}
121+
} else {
122+
/* Any pointer produced by a non-base caster will generally point
123+
into storage owned by the caster, which won't live long enough.
124+
Exception: the 'char' caster produces a result that points to
125+
storage owned by the incoming Python 'str' object, so it's OK. */
126+
static_assert(!is_ref || std::is_same_v<T, const char*>,
127+
"nanobind generally cannot produce objects that "
128+
"contain interior pointers T* (or references T&) if "
129+
"the pointee T is not handled by nanobind's regular "
130+
"class binding mechanism. For example, you can write "
131+
"a function that accepts int*, or std::vector<int>, "
132+
"but not std::vector<int*>.");
133+
}
134+
return flags;
135+
}
136+
91137
template <typename T>
92138
struct type_caster<T, enable_if_t<std::is_arithmetic_v<T> && !is_std_char_v<T>>> {
93139
NB_INLINE bool from_python(handle src, uint8_t flags, cleanup_list *) noexcept {
@@ -162,6 +208,7 @@ template <> struct type_caster<void_type> {
162208

163209
template <> struct type_caster<void> {
164210
template <typename T_> using Cast = void *;
211+
template <typename T_> static constexpr bool can_cast() { return true; }
165212
using Value = void*;
166213
static constexpr auto Name = const_name("types.CapsuleType");
167214
explicit operator void *() { return value; }
@@ -253,10 +300,15 @@ template <> struct type_caster<char> {
253300
return PyUnicode_FromStringAndSize(&value, 1);
254301
}
255302

303+
template <typename T_>
304+
NB_INLINE bool can_cast() const noexcept {
305+
return std::is_pointer_v<T_> || (value && value[0] && value[1] == '\0');
306+
}
307+
256308
explicit operator const char *() { return value; }
257309

258310
explicit operator char() {
259-
if (value && value[0] && value[1] == '\0')
311+
if (can_cast<char>())
260312
return value[0];
261313
else
262314
throw next_overload();
@@ -270,7 +322,8 @@ template <typename T> struct type_caster<pointer_and_handle<T>> {
270322

271323
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
272324
Caster c;
273-
if (!c.from_python(src, flags, cleanup))
325+
if (!c.from_python(src, flags_for_local_caster<T*>(flags), cleanup) ||
326+
!c.template can_cast<T*>())
274327
return false;
275328
value.h = src;
276329
value.p = c.operator T*();
@@ -305,13 +358,10 @@ template <typename T, typename... Ts> struct type_caster<typed<T, Ts...>> {
305358

306359
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
307360
Caster caster;
308-
if (!caster.from_python(src, flags, cleanup))
309-
return false;
310-
try {
311-
value = caster.operator cast_t<T>();
312-
} catch (...) {
361+
if (!caster.from_python(src, flags_for_local_caster<T>(flags), cleanup) ||
362+
!caster.template can_cast<T>())
313363
return false;
314-
}
364+
value = caster.operator cast_t<T>();
315365
return true;
316366
}
317367

@@ -410,6 +460,11 @@ template <typename Type_> struct type_caster_base : type_caster_base_tag {
410460
}
411461
}
412462

463+
template <typename T_>
464+
bool can_cast() const noexcept {
465+
return std::is_pointer_v<T_> || (value != nullptr);
466+
}
467+
413468
operator Type*() { return value; }
414469

415470
operator Type&() {
@@ -433,58 +488,87 @@ template <bool Convert, typename T>
433488
T cast_impl(handle h) {
434489
using Caster = detail::make_caster<T>;
435490

491+
// A returned reference/pointer would usually refer into the type_caster
492+
// object, which will be destroyed before the returned value can be used,
493+
// so we prohibit it by default, with two exceptions that we know are safe:
494+
//
495+
// - If we're casting to a bound object type, the returned pointer points
496+
// into storage owned by that object, not the type caster. Note this is
497+
// only safe if we don't allow implicit conversions, because the pointer
498+
// produced after an implicit conversion points into storage owned by
499+
// a temporary object in the cleanup list, and we have to release those
500+
// temporaries before we return.
501+
//
502+
// - If we're casting to const char*, the caster was provided by nanobind,
503+
// and we know it will only accept Python 'str' objects, producing
504+
// a pointer to storage owned by that object.
505+
506+
constexpr bool is_ref = std::is_reference_v<T> || std::is_pointer_v<T>;
436507
static_assert(
437-
!(std::is_reference_v<T> || std::is_pointer_v<T>) ||
438-
detail::is_base_caster_v<Caster> ||
508+
!is_ref ||
509+
is_base_caster_v<Caster> ||
439510
std::is_same_v<const char *, T>,
440511
"nanobind::cast(): cannot return a reference to a temporary.");
441512

442513
Caster caster;
443514
bool rv;
444-
if constexpr (Convert) {
445-
cleanup_list cleanup(nullptr);
515+
if constexpr (Convert && !is_ref) {
516+
// Release the values in the cleanup list only after we
517+
// initialize the return object, since the initialization
518+
// might access those temporaries.
519+
struct raii_cleanup {
520+
cleanup_list list{nullptr};
521+
~raii_cleanup() { list.release(); }
522+
} cleanup;
446523
rv = caster.from_python(h.ptr(),
447524
((uint8_t) cast_flags::convert) |
448525
((uint8_t) cast_flags::manual),
449-
&cleanup);
450-
cleanup.release(); // 'from_python' is 'noexcept', so this always runs
526+
&cleanup.list);
527+
if (!rv)
528+
detail::raise_cast_error();
529+
return caster.operator cast_t<T>();
451530
} else {
452531
rv = caster.from_python(h.ptr(), (uint8_t) cast_flags::manual, nullptr);
532+
if (!rv)
533+
detail::raise_cast_error();
534+
return caster.operator cast_t<T>();
453535
}
454-
455-
if (!rv)
456-
detail::raise_cast_error();
457-
return caster.operator detail::cast_t<T>();
458536
}
459537

460538
template <bool Convert, typename T>
461539
bool try_cast_impl(handle h, T &out) noexcept {
462540
using Caster = detail::make_caster<T>;
463541

464-
static_assert(!std::is_same_v<const char *, T>,
465-
"nanobind::try_cast(): cannot return a reference to a temporary.");
542+
// See comments in cast_impl above
543+
constexpr bool is_ref = std::is_reference_v<T> || std::is_pointer_v<T>;
544+
static_assert(
545+
!is_ref ||
546+
is_base_caster_v<Caster> ||
547+
std::is_same_v<const char *, T>,
548+
"nanobind::try_cast(): cannot return a reference to a temporary.");
466549

467550
Caster caster;
468551
bool rv;
469-
if constexpr (Convert) {
552+
if constexpr (Convert && !is_ref) {
470553
cleanup_list cleanup(nullptr);
471554
rv = caster.from_python(h.ptr(),
472555
((uint8_t) cast_flags::convert) |
473556
((uint8_t) cast_flags::manual),
474-
&cleanup);
557+
&cleanup) &&
558+
caster.template can_cast<T>();
559+
if (rv) {
560+
out = caster.operator cast_t<T>();
561+
}
475562
cleanup.release(); // 'from_python' is 'noexcept', so this always runs
476563
} else {
477-
rv = caster.from_python(h.ptr(), (uint8_t) cast_flags::manual, nullptr);
478-
}
479-
480-
if (rv) {
481-
try {
482-
out = caster.operator detail::cast_t<T>();
483-
return true;
484-
} catch (const builtin_exception&) { }
564+
rv = caster.from_python(h.ptr(), (uint8_t) cast_flags::manual, nullptr) &&
565+
caster.template can_cast<T>();
566+
if (rv) {
567+
out = caster.operator cast_t<T>();
568+
}
485569
}
486570

487-
return false;
571+
return rv;
488572
}
489573

490574
NAMESPACE_END(detail)

include/nanobind/nb_lib.h

+7-2
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,13 @@ NB_CORE PyObject *nb_type_put_unique_p(const std::type_info *cpp_type,
298298
void *value, cleanup_list *cleanup,
299299
bool cpp_delete) noexcept;
300300

301-
/// Try to reliquish ownership from Python object to a unique_ptr
302-
NB_CORE void nb_type_relinquish_ownership(PyObject *o, bool cpp_delete);
301+
/// Try to reliquish ownership from Python object to a unique_ptr;
302+
/// return true if successful, false if not. (Failure is only
303+
/// possible if `cpp_delete` is true.)
304+
NB_CORE bool nb_type_relinquish_ownership(PyObject *o, bool cpp_delete) noexcept;
305+
306+
/// Reverse the effects of nb_type_relinquish_ownership().
307+
NB_CORE void nb_type_restore_ownership(PyObject *o, bool cpp_delete) noexcept;
303308

304309
/// Get a pointer to a user-defined 'extra' value associated with the nb_type t.
305310
NB_CORE void *nb_type_supplement(PyObject *t) noexcept;

include/nanobind/stl/detail/nb_array.h

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ template <typename Array, typename Entry, size_t Size> struct array_caster {
2121
Caster caster;
2222
bool success = o != nullptr;
2323

24-
if constexpr (is_base_caster_v<Caster> && !std::is_pointer_v<Entry>)
25-
flags |= (uint8_t) cast_flags::none_disallowed;
24+
flags = flags_for_local_caster<Entry>(flags);
2625

2726
if (success) {
2827
for (size_t i = 0; i < Size; ++i) {
29-
if (!caster.from_python(o[i], flags, cleanup)) {
28+
if (!caster.from_python(o[i], flags, cleanup) ||
29+
!caster.template can_cast<Entry>()) {
3030
success = false;
3131
break;
3232
}

0 commit comments

Comments
 (0)