Skip to content

fix use-after-free of a blackboard entry held by getAnyLocked (alternative to #1181) - #1202

Merged
facontidavide merged 3 commits into
masterfrom
fix-blackboard-entry-lifetime
Sep 20, 2026
Merged

facontidavide merged 3 commits into
masterfrom
fix-blackboard-entry-lifetime

Conversation

@facontidavide

Copy link
Copy Markdown
Collaborator

Alternative to #1181, same bug, smaller and ABI-transparent fix. The AnyPtrLocked* regression tests are taken from #1181 (thanks @aysha-afrah26, credited as co-author).

Bug

Blackboard::getAnyLocked() returns an AnyPtrLocked: a raw Any* plus the raw, locked entry_mutex of a Blackboard::Entry. Nothing keeps the Entry alive, so unset(), clear(), cloneInto() or ~Blackboard can destroy it while the handle is in use: heap-use-after-free (seen on the Groot2 thread, ExportBlackboardToJSON vs an UnsetBlackboard node).

LockedPtr can not become an owning handle: it is returned by value from exported functions and its ctor/dtor are inlined in user binaries, so its layout (two raw pointers) is ABI.

Fix

Attach a custom deleter to every Entry at its only two allocation sites (both in src/blackboard.cpp). When the last shared_ptr is dropped:

  • mutex free and nothing parked (the normal case): delete immediately, no lock, no allocation;
  • mutex still locked by a holder: the entry is parked and destroyed by a later sweep (next entry retirement or creation) that finds it unlocked.

Compared with #1181:

  • Every removal path is covered with no change to it, including the unset() and ~Blackboard already inlined into binaries built against the 4.10 headers: they get the fix by just upgrading the library. A future removal path can not forget the protocol.
  • getAnyLocked() is untouched (no second lookup / retry loop on the read path).
  • Source change is +66 lines in blackboard.cpp; no layout or symbol change.

Also in this PR (from review): unset(), clear() and cloneInto() now destroy the removed values after releasing storage_mutex_; doc note in LockedPtr that the pointer is not valid after unlock().

Known limits (shared with #1181, inherent to the 16-byte handle)

  • The sweep may call try_lock() on a mutex owned by the calling thread (holder and remover on the same thread). Formally undefined for std::mutex, but it simply fails on the supported platforms; documented in the code. TSan does not object.
  • unlock() -> removal -> lock() on the same handle is still unsafe. A real fix needs an owning handle, i.e. an ABI break (BT.CPP 5).
  • A parked value's destructor is delayed until the next entry creation/retirement.

Verification

🤖 Generated with Claude Code

facontidavide and others added 2 commits September 20, 2026 14:37
AnyPtrLocked holds raw pointers into a Blackboard::Entry and can not own it
without changing its layout (ABI). Attach a custom deleter to every Entry at
its two allocation sites: an entry whose last shared_ptr is dropped while its
mutex is still locked is parked, and destroyed by a later retirement that
finds it unlocked.

Being attached at allocation time, the deleter covers unset(), clear(),
cloneInto() and ~Blackboard with no change to them, including the unset()
already inlined in binaries built against the 4.10 headers. No header change.

Alternative to #1181; the AnyPtrLocked* regression tests come from that PR.

Co-Authored-By: Aysha Afrah Ziya <aysha26@digiscrypt.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the storage lock

Changes requested by two independent reviews:
- RetireEntry is noexcept, and neither locks nor allocates when nothing is parked
- unset(), clear() and cloneInto() destroy the removed values after releasing
  storage_mutex_
- parked entries are also swept when a new entry is created, to bound the
  delay of the stored value's destructor
- document that LockedPtr is not valid after unlock()

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@facontidavide
facontidavide merged commit 9d6ffbf into master Sep 20, 2026
15 of 16 checks passed
@facontidavide
facontidavide deleted the fix-blackboard-entry-lifetime branch September 20, 2026 13:16
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
B Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant