CesiumGS / CesiumGS/cesium-native

TileLoadInput's pAssetAccessor reference member dangles in CesiumIonTilesetLoader / ITwinRealityDataContentLoader: it binds to a derived-to-base conversion temporary

Open Beginner friendly
#1,421 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
627
Forks
279
PR merge metrics
No merged PRs in 30d

Description

Summary

TileLoadInput stores pAssetAccessor as a reference member (const std::shared_ptr<CesiumAsync::IAssetAccessor>&). Two aggregated loaders construct a TileLoadInput by passing a derived-typed shared_ptr member directly:

  • CesiumIonTilesetLoader::loadTileContent passes this->_pIonAccessor (a shared_ptr<CesiumIonAssetAccessor>)
  • ITwinRealityDataContentLoader::loadTileContent passes this->_pRealityDataAccessor (a shared_ptr<RealityDataAssetAccessor>)

shared_ptr<Derived> and shared_ptr<Base> are distinct types, so the compiler materializes a temporary shared_ptr<IAssetAccessor> for the conversion and the reference member binds to that temporary. A reference member initialized through a constructor parameter gets no lifetime extension ([class.temporary]), so the temporary dies at the semicolon of the construction statement — before the aggregated input is ever used. Every downstream read of loadInput.pAssetAccessor in the aggregated loader (e.g. TilesetJsonLoader::loadTileContent) then goes through a dangling reference to a dead stack slot.

Both call sites are present on current main and in v0.59.0 (where we hit it).

The defect in code

// Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetContentLoader.h
struct CESIUM3DTILESSELECTION_API TileLoadInput {
  ...
  const std::shared_ptr<CesiumAsync::IAssetAccessor>& pAssetAccessor;  // reference member
  ...
};

// Cesium3DTilesSelection/src/CesiumIonTilesetLoader.cpp
TileLoadInput aggregatedInput(
    loadInput.tile,
    loadInput.contentOptions,
    loadInput.asyncSystem,
    this->_pIonAccessor,   // shared_ptr<CesiumIonAssetAccessor> → const shared_ptr<IAssetAccessor>&
    this->_pLogger,
    loadInput.requestHeaders,
    loadInput.ellipsoid);
// equivalent to binding the member to:
//   std::shared_ptr<IAssetAccessor>(this->_pIonAccessor)   ← unnamed temporary, dies at the ';'
return this->_pAggregatedLoader->loadTileContent(aggregatedInput);  // reads the dead slot

The synchronous call does not save it: aggregatedInput itself lives in the enclosing frame, but the reference member's target has sub-statement lifetime — a dead slot inside a live frame.

For contrast, the construction site in TilesetContentManager.cpp passes _externals.pAssetAccessor, a long-lived member of the exact parameter type, so no temporary is created there. The reference-member design silently makes safety depend entirely on each call site.

Observed impact

In ordinary builds the dead slot's bytes usually survive until the read, so the UB passes silently (the pointee is still owned by the member, and the stale control-block pointer "happens to work"). When the slot is reused within the window, a shared_ptr copied from garbage lands in the long-lived TileLoadResult and corrupts the heap on its later destruction — detonating far from the cause (in our case as allocator/FORTIFY aborts during engine teardown), with strong device/stack-layout dependence.

Deterministic reproduction (HWASan)

On an Android arm64 device (Pixel 8, Android 17) with full HWASan instrumentation (-fsanitize=hwaddress):

Build Result
Uninstrumented, unfixed 65 map open/close cycles, zero crashes (silent UB every cycle)
HWASan, unfixed deterministic crash on cycle 1, 2/2 runs: stack tag-mismatch, READ of size 8 in shared_ptr<IAssetAccessor>'s copy ctor, reached from TilesetJsonLoader::loadTileContentCesiumIonTilesetLoader::loadTileContent
HWASan, fixed (named local, below) 20/20 cycles, zero reports

Suggested fix

Materialize a named local of the exact parameter type at both call sites so the reference binds to an lvalue that outlives the call:

const std::shared_ptr<CesiumAsync::IAssetAccessor> pIonAccessor =
    this->_pIonAccessor;
TileLoadInput aggregatedInput(..., pIonAccessor, ...);

No API change; the named local replaces the conversion temporary one-for-one. We are running this fix in production and are happy to open a PR with both call-site fixes.

Suggested hardening (separate consideration)

Call sites that bind temporaries to TileLoadInput's reference parameters could be caught at compile time by either:

  • annotating the constructor's reference parameters with [[clang::lifetimebound]] (warns on binding temporaries), or
  • = delete-ing rvalue shared_ptr overloads of the constructor, or
  • switching the two shared_ptr members to by-value (the reference members look like a deliberate perf choice for requestHeaders etc., but the two shared_ptrs are copied into TileLoadResult downstream anyway).

Environment

  • cesium-native v0.59.0 (defect confirmed still present on main by code inspection, 2026-07-20)
  • Android NDK clang, arm64-v8a; HWASan via -fsanitize=hwaddress -fno-omit-frame-pointer

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with TileLoadInput in Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetContentLoader.h, then inspect loadTileContent in CesiumIonTilesetLoader.cpp and ITwinRealityDataContentLoader.cpp. Verify both aggregated-input constructions bind pAssetAccessor to a named exact-type local, and use the reported Android HWASan reproduction to confirm the dangling-reference report is gone.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.