-
Notifications
You must be signed in to change notification settings - Fork 698
Description
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
sparseflag (introduced in Allow a 'sparse' option per data source #1558) mitigates this for the default case, since non-vector sources now default tosparse: true→callback(). However, explicitly settingsparse: falseon a raster-dem source still routes throughcreateEmptyResponse(), 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:
TileLoader→setData(nullptr)raster_dem_tile_worker.cpp→ skips image decodingraster_dem_tile.cpp→renderable = falserender_raster_dem_source.cpp→isRenderable()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).