Skip to content

Missing raster-dem tiles should always use callback(), not createEmptyResponse() — can cause segfault in maplibre-native #2065

@JeremyBYU

Description

@JeremyBYU

Describe the bug

When the internal renderer (serve_rendered.js) requests a tile from a local source and the tile doesn't exist, createEmptyResponse() generates a 1×1 pixel image. For raster-dem sources, this 1×1 PNG is passed to maplibre-native as valid DEM tile data. When DEMData::backfillBorder() later runs between this 1×1 tile and a real 256×256 neighbor, the dimension mismatch causes an out-of-bounds memory read — which may segfault, silently corrupt data, or appear to work depending on memory layout.

(Reported upstream as maplibre/maplibre-native#4160)

Affected versions

  • v5.3.0 and earlier: Always affected. Missing DEM tiles unconditionally go through createEmptyResponse().
  • v5.4.0+: The sparse flag (introduced in Allow a 'sparse' option per data source #1558) mitigates this for the default case, since non-vector sources now default to sparse: truecallback(). However, explicitly setting sparse: false on a raster-dem source still routes through createEmptyResponse(), triggering the same bug. Or setting sparse: false globally as well.

Root cause

createEmptyResponse() was designed for raster image sources where a 1×1 transparent image is visually harmless. For raster-dem sources, tile dimensions are critical — backfillBorder copies pixel rows between neighbors assuming identical dimensions. Sending a 1×1 image as DEM data is semantically wrong regardless of the sparse setting.

Suggested fix

In the fetchTile == null path, always use callback() for raster-dem sources, regardless of the sparse flag:

if (fetchTile == null) {
  const sparse = map.sparseFlags[sourceId] ?? true;
  if (sparse || sourceInfo.type === 'raster-dem') {
    callback();
    return;
  }
  createEmptyResponse(sourceInfo.format, sourceInfo.color, callback);
  return;
}

callback() with zero arguments takes maplibre-native's designed "no content" path:

  1. TileLoadersetData(nullptr)
  2. raster_dem_tile_worker.cpp → skips image decoding
  3. raster_dem_tile.cpprenderable = false
  4. render_raster_dem_source.cppisRenderable() returns false → backfill loop skipped

This is analogous to HTTP 204 semantics. The tile enters a loaded-but-empty state, the empty area gets the background color, and all neighbor interactions are safely skipped.

Why not callback() for all source types?

createEmptyResponse respects sourceInfo.color — a source definition can specify what color empty tiles should be. Changing this for non-DEM sources would be a breaking change. For raster-dem sources, sourceInfo.color is meaningless (DEM tiles are elevation data, not visual), so callback() is strictly correct.

Crash evidence

Segfault from dmesg:

node[PID]: segfault at <addr> ip <addr> error 4 in mbgl.node

addr2line resolved to mbgl::DEMData::backfillBorder in tested binaries (maplibre-native 6.0.0 and 6.3.0).

Additional context

Full write-up covering how this interacts with maplibre-native and maplibre-gl-js:
https://gist.github.com/JeremyBYU/f5fbd206dc1d55276ee8ab50f43ed83b

Related: maplibre/maplibre-native#4160 (the dimension guard that should also be added in maplibre-native as defense in depth).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions