)]}'
{
  "commit": "63121f7b01c88717971233a3937e8869c2947c57",
  "tree": "97617e7018ea190fc3ad57047d9b2c6870df5d52",
  "parents": [
    "80f435b8c79b4cab35b1151da1a6b768d19aa70d"
  ],
  "author": {
    "name": "Andrew James",
    "email": "Andrew.m.james2@gmail.com",
    "time": "Tue Sep 15 17:05:42 2026 -0500"
  },
  "committer": {
    "name": "GitHub",
    "email": "noreply@github.com",
    "time": "Tue Sep 15 15:05:42 2026 -0700"
  },
  "message": "fix: Narrow conditions for `load_value` to give invalid address (#6157)\n\n* fix: Guard against using a uninitialized value after `__new__` allocating python object\n\nfixes: #6153\n\nObjects initialized with `cls.__new__(cls)` (`cls` is a pybind11 bound\ntype). Will not have the C++ object allocated. When hitting `load_value`\nstorage is allocated but not initialized, calling a virtual method will\nload a garbage vptr and segfault. This is similar to #2152, but the\nguard in metaclass `__call__` is not triggered when using `__new__`.\n\nProtect against giving a pointer to garbage in all cases except the\n`__init__` + `__setstate__` path.\n\nAuthored with claude\n\n* fix: free lazily allocated storage on failed init and only permit lazy allocation for old-style constructors\n\nIf an old-style placement-new `__init__`/`__setstate__` failed after\n`self` was loaded, the lazily allocated storage stayed behind with a\nnull-holder instance, so the uninitialized-value guard never fired again\nand later use read uninitialized memory. `instance_construction_scope`\nnow tracks the constructor\u0027s `value_and_holder` and frees storage that\nwas lazily allocated during a construction that did not complete.\n\nAlso arm the scope only when the overload chain contains an old-style\nconstructor. New-style constructors receive `self` directly and never\nneed lazy allocation, so reentrant loads of the half-built instance now\nraise `ValueError` instead of handing out uninitialized storage.\n\nAssisted-by: ClaudeCode:claude-fable-5\nClaude-Session: https://claude.ai/code/session_01TQXCSykMn5EL7sc6VgTUTC\n\n* fix: isolate old-style constructor storage\n\nTrack construction per value-and-holder, grant a one-shot loader-frame permission only to the exact legacy constructor self conversion, and keep its raw storage private until the native callback returns.\n\nReject reentrant, nested, cross-base, and cross-thread loads while preserving overload fallback, failure cleanup, pickle setstate callbacks, and repeated initialization behavior.\n\n* test: skip constructor thread test on Emscripten\n\n* fix: bump internals version to 13\n\nThe new detail::instance construction state has cross-DSO semantics that internals-v12 modules do not understand. Isolate the incompatible domains for v3.2.0 and document that future structural or semantic instance changes require another bump.\n\n* Revert \"fix: bump internals version to 13\"\n\nThis reverts commit 14e32ae23af529df8d82681c2d3064884b259a3c.\n\n* fix: recover from legacy constructor storage collisions\n\n* test: fix collision subprocess imports\n\n* refactor: simplify old-style constructor storage tracking\n\nThe loader frame already identifies the constructor candidate, so the\none-shot `self` permission only needs a frame match and a claimed flag.\nThis removes both argument guard classes, the changes to cast.h, and the\nper-call TLS lookups they added.\n\nAlso:\n- Hoist deallocate_instance_value to a detail free function and use it\n  from instance_construction_scope.\n- Take the dispatcher\u0027s constructor lock before the construction scope and\n  drop the nested critical sections it made redundant.\n- Keep the non-constructor path inline: the loader destructor checks for\n  storage before the out-of-line cleanup, and the construction scope\n  defaults to not started.\n- Commit old-style storage once in cpp_function::initialize, gated on\n  is_constructor.\n- Share one __index__ probe across the reentrancy tests and turn the\n  subprocess script into a plain function.\n\nAssisted-by: ClaudeCode:claude-fable-5-1\n\n* test: reject later self alias during old-style init\n\n* fix: restrict old-style constructor self permission to self\u0027s own load phase\n\nThe one-shot `self` permission granted by `loader_life_support` matched only the\nframe and the value slot, not the *phase* of the load. The slot is identified by\nthe instance, so any later argument that aliases the same, still-unconstructed\n`self` matched too, consumed the reservation, and reached C++ over raw storage.\n\nTrack the frame\u0027s phase instead: authorize the load of positional argument 0\n(the typed-`self` variant) and casts performed from within the C++ callable (the\nlegacy `py::object`-self variant), and deny every load during conversion of\npositional arguments \u003e\u003d 1. `argument_loader` reports the argument index, and the\ndispatcher flips the frame to the callable phase once loading is done. The frame\npointer is resolved once in the dispatcher, where `is_constructor` is known, so\nno non-constructor call pays a thread-local lookup.\n\nThis restores the restriction that 89a5f72e dropped, re-enabling the regression\ntest added in 7a7e9f34, and adds three more tests. Both new negative tests\nassert the decisive observable rather than only that the callback was entered,\nand both use types chosen so that a build where the guard has regressed reports\nan assertion failure instead of crashing during teardown:\n\n- A later argument typed as a *base* of the class under construction. It shares\n  the value slot, so it matched, and the reservation was then sized from the\n  base\u0027s `type_info`: 16 bytes for a 144-byte derived object. Because the claim\n  is one-shot it also denied `self` its own storage, so the callback could not\n  placement-new at all; the value was nevertheless committed, and destroying it\n  ran a virtual destructor over never-constructed memory. The test asserts that\n  no reservation was made at the base\u0027s size.\n- The still-unconstructed `self` reached through a container argument. Here\n  `stl.h`\u0027s element caster copy-constructs, so the read of uninitialized memory\n  happens inside pybind11 and no binding author can guard against it. The test\n  counts copy constructions whose source was raw storage and asserts zero; the\n  instrumented copy constructor does not read its source, so the test itself\n  performs no uninitialized read.\n- A positive test pinning the case that must keep working: a later argument that\n  is a different, already-constructed instance of the same class.\n\nAssisted-by: ClaudeCode:claude-opus-5\n\n* style: pre-commit fixes\n\n* style: clang-tidy fixes\n\nThe Clang-Tidy job failed on two `modernize-use-default-member-init`\ndiagnostics in tests/test_class.cpp, both introduced by this branch:\n\n    tests/test_class.cpp:167:18: error: use default member initializer\n      for \u0027payload\u0027 [modernize-use-default-member-init,-warnings-as-errors]\n    tests/test_class.cpp:177:9: error: use default member initializer\n      for \u0027value\u0027 [modernize-use-default-member-init,-warnings-as-errors]\n\nApplied exactly the replacements clang-tidy emitted:\n\n- `AliasStealDerived::payload` gains a `{}` default member initializer\n  and drops `payload{}` from the constructor initializer list. Both\n  value-initialize the array.\n- `ContainerAliasItem::value` gains a `{-1}` default member initializer\n  and the copy constructor drops `value(-1)`. The converting constructor\n  keeps `value(v)`, which overrides the default, so both constructors\n  still produce the values the container-alias test asserts on.\n\nNo behavior change; clang-tidy is not installed locally, so the fix-its\nwere transcribed from the CI diagnostic rather than auto-applied, and\nthe translation unit was compiled clean at -std\u003dc++17 with the CI\nwarning set.\n\nAssisted-by: ClaudeCode:claude-opus-5\n\n* fix: GraalPY exceptions\n\nBoth GraalPy jobs failed on the same assertion in the legacy-v12\ncollision test:\n\n    tests/test_class.py:469: assert stats() \u003d\u003d (3, 3, constructed + 1, constructed + 1)\n\nThat is the final check, reached after `del obj` and two `gc.collect()`\ncalls. GraalPy is not refcounted and does not guarantee finalization\nfrom `gc.collect()`, so the destruction counters lag and the assertion\nfails while every preceding assertion in the loop passes.\n\nGate only that assertion behind `if not env.GRAALPY:`. This follows the\nexisting convention in the suite, where GC-timing-dependent checks are\nexempted on GraalPy with the same \"Cannot reliably trigger GC\" reason\n(test_call_policies.py, test_callbacks.py,\ntest_class_sh_trampoline_shared_ptr_cpp_arg.py, and others).\n\nNothing this branch introduces stops being tested on GraalPy. The gated\nline only observes ordinary teardown of a normal, fully constructed\nretry object. The rollback properties the test exists for are pinned by\nthe assertions above it, which still run everywhere: after rollback both\ncollision allocations are freed with no spurious destruction, and after\nthe retry exactly one allocation is live with the private value\u0027s\ndestructor having run neither early nor twice.\n\nAssisted-by: ClaudeCode:claude-opus-5\n\n* test: pin the two gaps identified in the load-phase review\n\nAdds coverage for the two limitations called out in the review of the\nload-phase restriction. Both tests pass, pinning today\u0027s behavior; both\nfail against `master`\u0027s headers, which is what makes them meaningful.\n\ntest_old_style_init_value_error_hides_later_overload\n\n  Two old-style candidates take the same two Python arguments. The\n  first one\u0027s argument 1 is the `self` alias that the construction\n  guard rejects; the second matches the same call and constructs.\n  The guard reports rejection with `value_error`, and only\n  `reference_cast_error` becomes PYBIND11_TRY_NEXT_OVERLOAD, so the\n  throw escapes the overload loop and the second candidate is never\n  attempted.\n\n  Verified counterfactual, same test files built against master\u0027s\n  headers: master reaches the second candidate and constructs\n  (`entered \u003d\u003d [\"second candidate entered\"]`); here the call raises\n  ValueError with `entered \u003d\u003d []`.\n\n  Note this is a new trigger for pre-existing behavior rather than a\n  new behavior: master\u0027s casters already throw `value_error` from load\n  paths with the same non-fallthrough consequence.\n\ntest_old_style_init_callable_phase_grant_is_not_self_specific\n\n  While the callable runs, the one-shot grant is keyed on the value\n  slot, not on the `self` handle, so a cast of `stash[0]` claims the\n  reservation and the genuine `self` cast then fails. Narrowing the\n  grant to \"a cast of the `self` object\" would not close this: the\n  claiming cast targets the same Python object as `self`, so the two\n  are indistinguishable at cast time.\n\n  Verified counterfactual: on master both casts succeed\n  (`[\"stash cast claimed the reservation\", \"self cast succeeded\"]`)\n  because every load lazily allocates. The one-shot reservation is\n  therefore a narrowing of master\u0027s behavior, and this gap is the\n  residue rather than a regression.\n\nNeither callback inspects the reference it obtains over storage whose\nlifetime has not begun, so the tests themselves stay free of undefined\nbehavior. Both verify the object is still retryable afterwards.\n\nAssisted-by: ClaudeCode:claude-opus-5\n\n* perf: only test the old-style frame pointer where the phase can change\n\nAddresses the review suggestion to stop paying the null check once per\nargument.\n\nThe literal form suggested, `I \u003d\u003d 0 \u0026\u0026`, is not safe: `begin_argument_load`\nis what moves the frame from `self_argument` to `later_argument`, so\nskipping it for arguments 1 and up leaves the phase at `self_argument` for\nthe whole argument list. That re-opens exactly the hole 955cb193 closed. It\nregresses four tests:\n\n    test_old_style_init_does_not_authorize_later_self_alias\n    test_old_style_init_does_not_authorize_base_typed_later_alias\n    test_old_style_init_does_not_authorize_self_alias_inside_container\n    test_old_style_init_value_error_hides_later_overload\n\nGate on `I \u003c 2` instead. The phase only changes at argument 0 and argument\n1; from argument 2 on it is already `later_argument`, so those arguments\nneed no call and no test. Two checks per call rather than one, but it is\nthe minimum that preserves the invariant. All 55 tests pass.\n\n`I` is a template parameter, so no `if constexpr` is needed and none can be\nused: pybind11 still supports C++11 and `if constexpr` is a C++17\nextension there. A plain `if` on a constant condition already folds\ncompletely. clang -O2, the `I \u003d\u003d 5` instantiation of a reduction of this\nfunction tail-calls straight through with no pointer test emitted, while\n`I \u003d\u003d 0` and `I \u003d\u003d 1` keep theirs.\n\nMSVC C4127 (constant conditional) is already disabled file-wide at the top\nof cast.h, and the header compiles clean at -std\u003dc++11/14/17/20 with\n-Wall -Wextra -Wpedantic -Wconversion -Werror.\n\nAssisted-by: ClaudeCode:claude-opus-5\n\n* docs: describe status_value_constructing with the other status bits\n\nThe non-simple layout comment enumerated status_holder_constructed and\nstatus_instance_registered but not status_value_constructing, which was\nadded alongside them. Addresses the review comment on that block.\n\nAlso states what the bit means for readers of the value pointer: while it\nis set, the pointer must not be treated as denoting a live C++ object.\nThat is the invariant the rest of this change depends on, and the status\nbyte is where someone will look for it.\n\nComment-only. Longest line is 94 columns, within the 99-column limit, so\nclang-format does not reflow it.\n\nAssisted-by: ClaudeCode:claude-opus-5\n\n* style: pre-commit fixes\n\n* style: clang-tidy/format\n\n* fix: narrow uninitialized-instance guard to direct misuse\n\nReturn to the minimal scope needed for #6153: ordinary loads of a wrapper with no constructed C++ value raise ValueError, while overload chains containing deprecated placement-new constructors retain their historical lazy allocation. Failed old-style construction also frees lazily allocated storage so the object remains guarded and retryable.\n\nRemove the expanded per-value construction protocol, including private candidate storage, argument and callable phases, cross-thread locking, and stale-v12 collision recovery. Those mechanisms attempted to make deprecated placement-new construction safe under reentry rather than fixing direct __new__ misuse.\n\nAccordingly, remove tests requiring special handling for later arguments (both distinct instances and self aliases), base-typed and container aliases, nested initialization, Python multiple-inheritance bases, concurrent access, mixed old/new overloads, and old-style __setstate__ reentry. Also remove tests pinning protocol-specific overload fallthrough, callable-phase grants, and legacy-v12 collision cleanup.\n\nKeep focused coverage for direct __new__ misuse, reentry during new-style construction, deprecated __init__/__setstate__ compatibility, failed-construction cleanup, and successful retry.\n\n* test: characterize old-style reentrant load limitation\n\nKeep one pointer-only probe for the historical broad lazy-allocation window. It records the known hazard that a reentrant load can expose a pointer to unconstructed storage, without inspecting or dereferencing that storage.\n\nThe cleanup and retry assertions remain in place so the behavior retained by the minimal fix stays covered.\n\n* docs: document deprecated placement-new limitations\n\nExplain that the compatibility window spans an entire constructor overload chain and can expose unconstructed storage during reentrant conversion or callbacks, nested initialization, Python multiple inheritance, or concurrent access.\n\nRecommend new-style constructor and pickle APIs, and add source-level references at the compatibility flag and scope so future changes encounter the accepted limitations and rationale before attempting to narrow the window.\n\n---------\n\nCo-authored-by: Henry Schreiner \u003chenryfs@princeton.edu\u003e\nCo-authored-by: Ralf W. Grosse-Kunstleve \u003crgrossekunst@nvidia.com\u003e\nCo-authored-by: Andrew M. James \u003cajames@openteams.com\u003e\nCo-authored-by: pre-commit-ci[bot] \u003c66853113+pre-commit-ci[bot]@users.noreply.github.com\u003e",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "2954411d7bf7269f55e47035fe44b5188871facc",
      "old_mode": 33188,
      "old_path": "docs/advanced/classes.rst",
      "new_id": "a5de5b1efa42ffef38cbce0bc0a95af2b0b7199e",
      "new_mode": 33188,
      "new_path": "docs/advanced/classes.rst"
    },
    {
      "type": "modify",
      "old_id": "966a319a80bf5c813d37137a83a93c2688ae0fad",
      "old_mode": 33188,
      "old_path": "docs/upgrade.rst",
      "new_id": "236494a5cdd58d8841fd685c46289ce498feaf84",
      "new_mode": 33188,
      "new_path": "docs/upgrade.rst"
    },
    {
      "type": "modify",
      "old_id": "9f8b3b32663fee50a3908a29d291cd4fbe9335f3",
      "old_mode": 33188,
      "old_path": "include/pybind11/detail/common.h",
      "new_id": "7738b3f39bb294d70c73e13b0a58eeaaf65b2616",
      "new_mode": 33188,
      "new_path": "include/pybind11/detail/common.h"
    },
    {
      "type": "modify",
      "old_id": "82bfa0b27ca36ddb67c5f13c0cd8ed83163c42ec",
      "old_mode": 33188,
      "old_path": "include/pybind11/detail/type_caster_base.h",
      "new_id": "65b6bd07dfe26db733bd4a3901e004a150ca1722",
      "new_mode": 33188,
      "new_path": "include/pybind11/detail/type_caster_base.h"
    },
    {
      "type": "modify",
      "old_id": "c57312d64d048be1f91ea4b0e223727591fed372",
      "old_mode": 33188,
      "old_path": "include/pybind11/pybind11.h",
      "new_id": "e9c0e903ebd981a76b9ed5bed243ce63c536cc08",
      "new_mode": 33188,
      "new_path": "include/pybind11/pybind11.h"
    },
    {
      "type": "modify",
      "old_id": "e520f29ec5fc36a9e8acf8105aa2a4a3b190ee9d",
      "old_mode": 33188,
      "old_path": "tests/test_class.cpp",
      "new_id": "21ff617365ddb248aeffbe8ed5c420cb02435079",
      "new_mode": 33188,
      "new_path": "tests/test_class.cpp"
    },
    {
      "type": "modify",
      "old_id": "201c7e339e36d0587ac24bc6e1cf07d4afe6315d",
      "old_mode": 33188,
      "old_path": "tests/test_class.py",
      "new_id": "645b799c9f98acf3736dd13c52d8441583af540c",
      "new_mode": 33188,
      "new_path": "tests/test_class.py"
    }
  ]
}
