Fable 5.1 review of pybind11
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 18k
- Forks
- 2.3k
- Avg merge
- 5d 17h
- Merged PRs (30d)
- 10
Description
Finally got a proper report from Fable 5.1. I think my Fable 5 one hit a downgrade.
🤖 AI text below 🤖
Review of pybind11 master (d87cf0b8). Seven reviewers covered the headers, Python package, CMake, and CI. I verified every finding below against the source. Items marked "reproduced" were also exercised against the build tree by a reviewer. No files were changed. Reviewer repro scripts are in the session scratchpad.
Bugs
High
include/pybind11/pybind11.h:1156and:1209: when named args followpy::args, the*argsstub is pushed intocall.argsbut its convert flag is appended last. Each kw-only arg gets its successor's flag, sonoconvert()lands on the wrong argument. Reproduced. Fix: pushfalsewith the stub and append in step 4a only when appending the tuple.pybind11.h:1249: the second-pass loop stops atpos_args, so overloads whose only convertible args are keyword-only never get a converting pass. Reproduced. Iterate tosecond_pass_convert.size().include/pybind11/gil_safe_call_once.h:175-244: nothing resets the cached pointer or the initialized flag when an interpreter is finalized. Afterfinalize_interpreter(); initialize_interpreter()the fast path returns freed memory.py::register_exceptionuses this class, so re-importing an embedded module that registers an exception hits it. Reproduced with ASan. Clear the owner's cache from~call_once_storage.include/pybind11/detail/struct_smart_holder.h:347:from_unique_ptrbuildsshared_ptr<T>(raw, deleter), which always hooksenable_shared_from_this. The void-cast intent fromdetail/init.h:224is defeated, so factories returningunique_ptr<Trampoline>lose thebad_weak_ptrguard and the Python override can silently vanish. Regression from #5836, reproduced. Take an explicit flag and construct the owner asshared_ptr<void>.include/pybind11/eigen/matrix.h:181-194: strides that are not a multiple ofsizeof(Scalar)are truncated by integer division. A packed structured-field view maps as contiguous and a mutableRefwrites into the neighbouring field. Reproduced. Treat a non-divisible stride as non-conformable.include/pybind11/iostream.h:152:~pythonbuf()calls into Python; a failingwritethrows out of a destructor and terminates the process. Reproduced. Catch anddiscard_as_unraisable.CMakeLists.txt:387:cmake_path(RELATIVE_PATH ... BASE_DIRECTORY CMAKE_INSTALL_DATAROOTDIR)uses the literal string as the base, so an installedpybind11.pchas an emptyprefix=on CMake 3.20+. Confirmed in the current build tree. Wheels are unaffected because pyproject sets it. Use thewhile()branch unconditionally.
Medium
include/pybind11/cast.h:992,:1246,detail/type_caster_base.h:869: the alias flag is read fromsrc, but after an implicit conversion the loaded instance is the converter's temporary.srcmay not be a pybind11 instance at all. Record it inload_valuefromv_h.inst.cast.h:1738:py::castof an unregistered type returns a null object with a pending TypeError instead of throwing. Downstream use givesSystemError.cast.h:1219: returningunique_ptr<T>for ashared_ptr-held or custom-holderTcopy-constructs the holder from unique_ptr bytes.tinfo->holder_enum_vis available to throwcast_errorinstead.include/pybind11/detail/class.h:396:tp_allocresult is unchecked andallocate_layout()can throw out of theextern "C"tp_new. Callingpybind11_object()from Python aborts. Reproduced.include/pybind11/detail/common.h:90:__cpp_lib_launderis tested before<new>or<version>is included, sostd::launderis never used with libstdc++ in C++17 mode. Move the block below the includes.include/pybind11/stl.h:77-100,:199,:316,:451: thetp_nameheuristic matches any user class namedzip,map, ordict_keys; the followingassert(isinstance<iterable>)aborts debug builds and release builds throw instead of returning false. Reproduced. Reject heap types and return false.include/pybind11/stl_bind.h:470: stride-0 (broadcast) buffers giveend == p, so the buffer constructor returns an empty vector. Reproduced.iostream.h:251: calling__enter__twice destroys the first redirect while the second holds its buffer pointer, then segfaults. Reproduced.include/pybind11/chrono.h:46: days plus seconds plus microseconds is computed in int64 microseconds regardless of the target period, sotimedelta.maxoverflows. Line 188 does not compile for floating-point durations. Line 171 leavesmktimeunchecked.chrono.h:105: the Annex-Klocaltime_scall uses MSVC's argument order, so it is a compile error on any libc that actually provides Annex K. Uselocaltime_ron POSIX and drop the global mutex.include/pybind11/numpy.h:1453-1469:at()divides the byte offset byitemsize(), wrong for strides that are not multiples ofsizeof(T). Reproduced. Use*data(index...).numpy.h:1357: no negative-index check, soat(-1)reads before the buffer. Reproduced.numpy.h:785-793: thenumpy_scalarcaster uses function-local statics that runimport numpyunder the static-init guard, the GIL deadlock this header warns about elsewhere. Usegil_safe_call_once_and_store.include/pybind11/eigen/tensor.h:40: modulo byEIGEN_DEFAULT_ALIGN_BYTESis modulo zero withEIGEN_DONT_VECTORIZE.eigen/matrix.h:678-695: the sparse loader throws instead of returning false (the null checks are dead) and callsmutable_data()on inputs it only reads. Reproduced.include/pybind11/pytypes.h:2335:set::addreturns false with the error still set.stl.h:213then reports a generic conversion error instead of "unhashable type". Throw likelist::append.include/pybind11/detail/argument_vector.h:83,:377:ref_small_vectormove leaves the inline source with its size intact, so both destructors DECREF. Latent because the only user relies on copy elision, and the comment asserts the opposite.pybind11.h:1690:PyModule_GetFilenameObjectreturns a new reference stored in ahandle;def_submoduleleaks one per call. Reproduced.pybind11.h:3926:PYBIND11_OVERRIDE_IMPLuses a function-staticcaster for reference returns, a data race under free-threading.thread_localis the cheap fix.tools/pybind11Common.cmake:51withCMakeLists.txt:107: the CMP0190 cross-compiling default flips to OFF on reconfigure becauseoption()caches OFF and the "defaulted" marker is not cached. Reproduced.tools/pybind11Common.cmake:223: COMPAT-modePYTHON_*variables are plainset(), invisible to anadd_subdirectoryparent. Reproduced.tools/pybind11Common.cmake:206:PYBIND11_FINDPYTHON=OLDis truthy, so the documented value selects the NEW mode.tools/pybind11Common.cmake:375-395: the LTO fallbacks run even in the ppc64le/mips64 "do nothing" and Emscripten "no check" branches, andHAS_FLTO_THINis reused for thin and full probes so-flto=thinis never actually tested.
Low
pytypes.htuple, dict, list, set, int_, float_, bytes, and memoryview constructors callpybind11_failafter a NULL return, which asserts that no error is set.pytypes.h:1469:PYBIND11_OBJECT_CVTon a null object calls the converter with nullptr (segfault forbool_, silent emptydict).argument_vector.h:294: the assert should beidx < ha.size; exactly 64 kwargs aborts debug builds. Reproduced.detail/function_record_pyobject.h:145-186: rawtp_newandtp_methodsslots let C++ exceptions escape into CPython.pytypes.h:2067,:2137: capsule destructor lambdas throw from a C callback, and only one installserror_scope.type_caster_base.h:701-720: the new reference fromfind_registered_python_instanceis held in ahandleand leaked on thecast_errorpath.type_caster_base.h:545:get_object_handlereturns a borrowed pointer from the instance map, a use-after-free window under free-threading.cast.h:767:tuple_caster::loadkeeps subcaster pointers intoseq[Is]temporaries that die beforeimplicit_cast.detail/internals.h:517-527: nested-exception chaining forerror_already_setandbuiltin_exceptionis overwritten by the followingrestore()orset_error(). Reproduced.internals.h:702: the interpreter TLS key is updated before the state-dict fetch can throw, leaving the fast path pointing at the previous interpreter's internals.internals.h:708,:884:instance_basecheck-and-create andinternals_singleton_pp_are unsynchronized under free-threading.include/pybind11/conduit/pybind11_platform_abi_id.h:59:_GLIBCXX_DEBUGchanges container layouts ininternalsbut is not part of the ABI id.common.h:1163:format_descriptor<PyObject*>::valuehas no pre-C++17 out-of-class definition; undefined symbol in C++14 when ODR-used.struct_smart_holder.h:347: if the control-block allocation throws, the armed deleter and the still-owningunique_ptrboth delete.class.h:91,:579:pybind11_static_propertyon 3.12+ replacesproperty's GC slots, hidingfgetandfsetfrom the collector. Reproduced.class.h:141,:176:_PyType_LookupplusPy_INCREFraces under free-threading;_PyType_LookupRefexists on 3.13+.class.h:779:make_new_python_typeleaks the heap type on every failure path.pybind11.h:3688:PyErr_NewExceptionresult unchecked;pybind11.h:1735:PyModule_AddObjectresult ignored.numpy.h:1088,:1267:PyArray_NewCopy_andPyArray_Squeeze_NULL results unchecked.numpy.h:148,:1750:registered_dtypesaccessed without the internals lock.numpy.h:1415:array_t<T>(const buffer_info&)does not checkTagainst the format.eigen/tensor.h:158,:188,:384: non-defaultIndexTypedoes not compile; dtypes compared by identity rather than equivalence.include/pybind11/embed.h:95:PyConfig_SetBytesArgvruns unconditionally, clobbering a caller-populatedargv.subinterpreter.h:237,:344: two unchecked C-API results.CMakeLists.txt:172:.venvautodetect checksENV{VIRTUALENV}; the real variable isVIRTUAL_ENV.tools/pybind11NewTools.cmake:167andtools/FindPythonLibsNew.cmake:206:message(AUTHOR_WARNING, ...)has a stray comma, demoting it to a plain message..pre-commit-config.yaml:94: theremove-tabsexclude regex matches every path, so the hook is a no-op.tools/pybind11GuessPythonExtSuffix.cmake:75: the debug regex misses thetbeforedincpython-313td, and its test script is not run anywhere in CI..github/workflows/tests-cibw.yml: nopermissions:block. Third-party actions are tag-pinned, and the PyPI publish job withid-token: writeuses a moving branch ref.pybind11/setup_helpers.py:481:ParallelCompilepatches the basecompile, whichMSVCCompileroverrides, so Windows builds stay serial. Line 309 matchespackage_dirby rawstartswith.CMakeLists.txt:33: the version-type regex expects.dev1; the v3 scheme never matches.
Performance
detail/function_record_pyobject.h:93viapybind11.h:970: every dispatch callsget_function_record_PyTypeObject(), which takes the internals lock. A reviewer measured roughly 270 ns/call at 8 threads on 3.14t versus 50 ns with a direct cast.selfis always the record object, so cast directly indispatcher.type_caster_base.h:62:loader_life_supportholds anunordered_setby value; MSVC's default constructor allocates, so every call on Windows pays two allocations. Allocate lazily.type_caster_base.h:1198: every failed load of a registered type doesPyObject_HasAttrStringplusgetattrwith a long key, materializing an AttributeError on older CPython. Intern the key and use_PyType_Lookup.pytypes.h:2618:contains()goes throughattr("__contains__");PySequence_Containsis theinoperator and handles the iteration fallback.pytypes.h:1797:bytes(const str&)copies the UTF-8 buffer into a second bytes object.pytypes.h:1694:str::operator std::stringround-trips through a temporary bytes wherePyUnicode_AsUTF8AndSize(already used incast.h:522) would do.include/pybind11/functional.h:21:func_handlehas no move constructor, so loading onestd::functiondoes three GIL-acquiring copies and three GIL-acquiring destructions.pybind11.h:1413:keyword_indexre-interns the name on every lookup and never checks for NULL. Intern once at binding time (a function_record layout change, so bump the record ABI id).pybind11.h:3336:keep_alive_implcopies the type-info vector just to test.empty().numpy.h:2200:vectorizeruns the full buffer protocol per argument per call; line 2111 converts the F-style output through a secondPyArray_FromAny; the contiguity check does not skip length-1 dims, sox[:, None]misses the trivial path.eigen/matrix.h:488,:546: theRefloader heap-allocates aMapthatRefnever references again.stl.h:327:list_casteriterates viaPyIter_Nexteven for list/tuple.stl.h:107,:132importcollections.abcper miss.stl/filesystem.h:52importspathlibper cast.- CI:
tests/CMakeLists.txt:285clones full Eigen history in every job (tarball plus SHA256 is a few MB). No ccache anywhere.reusable-standard.yml:42still forces a Python download for a June 2025 issue.
Simplifications and modernizations
internals.h:45requires internals version 12+, so every>= 12and<= 11branch is dead (internals.h:311, 344, 424; class.h:229; type_caster_base.h:241-259; pybind11.h:1852, 2421).common.h:1387:PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100has been empty since #6110 but has 14 call sites.pybind11.h:3054-3100: thePYBIND11_ENUM_OP_*macros are defined and undefined with no uses since #5887.stl_bind.h:539-577: an#if 0block.stl_bind.h:136and:332duplicate thewrap_ilambda.stl.h:182clears the set twice.include/pybind11/gil.h:33-43: the forward declaration is redundant withinternals.h:148.pytypes.h:1438and:2713:get_scope_moduleandget_module_name_if_availableare byte-identical.pytypes.h:549,:999,:1039: dead error branches.pytypes.h:2224is the lastPyObject_CallFunctionObjArgs;pybind11.h:3637builds a 1-tuple forPyObject_Call. Both can usePyObject_CallOneArg.pytypes.h:1885:bool_::operator boolviaPyLong_AsLonginstead of comparing toPy_True.pytypes.h:532-825:PyErr_FetchandPyErr_Restorecould usePyErr_GetRaisedExceptionon 3.12+.type_caster_base.h:728: a no-op branch with a misleading comment.numpy.h:317: the "Unused" comment now sits on a used slot.common.h:110-138asserts clang 3.3 and gcc 4.8 floors while CI tests GCC 9+. The experimental filesystem and optional paths and the Eigen 3.2 fallbacks are never built by CI.cast.h:262,:580: PyPy 3.8-era workarounds worth retesting.tools/cmake_uninstall.cmake.in:12uses deprecatedexec_program.pybind11Common.cmake:277keeps apkg_resourcesfallback.pyproject.tomllists two classifiers twice and names ruff rules in per-file ignores that are never selected.tools/pybind11Config.cmake.indocuments variables that are never set.
Suggested order of attack: the two dispatcher bugs and the ref_small_vector move fix are small and self-contained; the interpreter-restart use-after-free, the smart_holder shared_from_this regression, and the Eigen stride truncation each need a regression test plus a focused fix; the pybind11.pc prefix and the CMP0190 reconfigure flip are one-line CMake fixes.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Begin by selecting one numbered finding and reading its cited file and line; reviewer repro scripts are in the session scratchpad, and reproduced items were exercised against the build tree, including some with ASan. Done means narrowing the review to a single confirmed defect, reproducing it, and verifying the corresponding fix without changing unrelated findings.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cmake, cpp, python
- Domain
- build-system, ci-cd, performance, testing-qa, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 15/100