Skip to content

Fix crash when a symbol layer without feature-state paint gets a pain… - #13721

Open
liueric-dev wants to merge 2 commits into
mapbox:mainfrom
liueric-dev:fix/13714-symbol-layer-feature-state-crash
Open

Fix crash when a symbol layer without feature-state paint gets a pain…#13721
liueric-dev wants to merge 2 commits into
mapbox:mainfrom
liueric-dev:fix/13714-symbol-layer-feature-state-crash

Conversation

@liueric-dev

Copy link
Copy Markdown

Fix crash when a symbol layer without feature-state paint gets a paint update

What changed

Tile#updateBuckets selected bucket.stateDependentLayers whenever withStateUpdates was true, but the guard that decides whether to use that array checked a wider, independently-satisfiable set of conditions (hasPaintUpdate, needsSymbolUBOUpdate). When a source carries state but none of a symbol bucket's layers actually read it in their paint, stateDependentLayers is empty, but the block can possibly still run because of an unrelated paint-property change. That passes an empty layers array ito SymbolBucket#update, whose UBO-based update path unconditionally does layers[0] as SymbolStyleLayer and dereferences .paint, throwing TypeError: Cannot read properties of undefined (reading 'paint').

Fixed by factoring the check into one hasStateDependentLayers value and reusing it everywhere stateDependentLayers is consulted, so the array that gets used and the condition that permits using it can never disagree.

Why this contribution

This is a real, currently-open, unclaimed bug report (#13714, labeled bug) with a clear, minimal repro: calling setFeatureState followed by setPaintProperty on a symbol layer with no ["feature-state", ...] expression in its paint crashes the map. I verified the other bucket types (fill/line/circle/fill-extrusion) aren't affected — only SymbolBucket has a UBO-binder path that indexes layers[0] unconditionally; the others iterate the array safely.

What problem it solves

Prevents an uncaught exception that breaks rendering entirely for any map that sets feature state on a source shared with a symbol layer that doesn't itself use feature-state in its paint — a fairly common pattern (e.g. a line layer using hover state, symbol layer on the same source not using it).

Testing

  • Added test/unit/source/tile.test.ts regression test: constructs a Tile + SymbolBucket with no state-dependent paint, calls updateBuckets() with feature state + a paint update, asserts it doesn't throw. Confirmed it fails without the fix and passes with it.
  • npm run tsc, npm run lint, and the full tile.test.ts suite all pass with no regressions.
  • Reproduced the original crash and confirmed the fix resolves it via a local debug page exercising the exact setFeatureState + setPaintProperty sequence from the issue.

Fixes #13714

Launch Checklist

  • Make sure the PR title is descriptive and preferably reflects the change from the user's perspective.
  • Add additional detail and context in the PR description (with screenshots/videos if there are visual changes).
  • Manually test the debug page.
  • Write tests for all new functionality and make sure the CI checks pass.
  • Document any changes to public APIs.
  • Post benchmark scores if the change could affect performance.
  • Tag @mapbox/map-design-team @mapbox/static-apis if this PR includes style spec API or visual changes.
  • Tag @mapbox/gl-native if this PR includes shader changes or needs a native port.
  • Tag @mapbox/gl-native if this PR disables any test because it also needs to be disabled on their side.
  • Create a ticket for gl-native to groom in the MAPSNAT JIRA queue if this PR includes shader changes or features not present in the native side or if it disables a test that's not disabled there.

…t update

Tile#updateBuckets picks bucket.stateDependentLayers whenever withStateUpdates
is true, but enters the update block under separately-checked conditions
(hasPaintUpdate, needsSymbolUBOUpdate). If a source carries feature state but
none of a bucket's layers actually read it in their paint (isStateDependent),
stateDependentLayers is empty, so a concurrent paint-property change (e.g.
setFeatureState followed by setPaintProperty) passes an empty layers array
into SymbolBucket#update. Its UBO-based update path unconditionally casts
layers[0] to SymbolStyleLayer and dereferences .paint, throwing when the
array is empty.

Factor the repeated withStateUpdates && stateDependentLayers.length !== 0
check into one hasStateDependentLayers value, reused for both the layers
selection and all three guards that depend on it, so the array chosen can
never disagree with the condition that lets it be used.

Fixes mapbox#13714

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@liueric-dev
liueric-dev requested a review from a team as a code owner August 27, 2026 00:24
@liueric-dev
liueric-dev requested review from ibesora and removed request for a team August 27, 2026 00:24
@github-actions

Copy link
Copy Markdown

Hey, @liueric-dev 👋 Thanks for your contribution to Mapbox GL JS!

Important: This repository does not accept direct merges. All changes go through our internal review process.

What happens next:

  1. A team member will review your PR here first
  2. If it looks good, they will import it to our internal repository for further review
  3. If approved, changes will be synced back here via our release process

Please respond to any review comments on this PR. For more details, see CONTRIBUTING.md.

The invariant it described belongs in the PR description, not the code —
it referenced the fix history rather than a non-obvious runtime constraint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

>= v3.21 Symbol layer without feature-state expression crash the map

2 participants