maplibre / maplibre/maplibre-react-native

createPack silently accepts a non-URL mapStyle and persists a pack that crashes the app on every later launch

Open
#1,646 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
661
Forks
124
Avg merge
1d 2h
Merged PRs (30d)
30

Description

## Summary

`OfflineManager.createPack({ mapStyle, ... })` accepts any `string` for `mapStyle` with no validation that it is actually a URL. If a caller passes something that type-checks as a `string` but is not a parseable URL — e.g. an already-serialised style JSON body, an easy mistake when a style is composed client-side rather than fetched from a single stable endpoint — neither native module rejects the call, and the failure mode is much worse than "the pack doesn't work": it silently persists a pack that **crashes the host app on every later launch**.

## What happens today (iOS)

`MLRNOfflineModule.mm`'s `createPack:`:

```objc
NSString *styleURL = options.mapStyle();
...
id offlineRegion =
[[MLNTilePyramidOfflineRegion alloc] initWithStyleURL:[NSURL URLWithString:styleURL]
bounds:bounds
fromZoomLevel:options.minZoom()
toZoomLevel:options.maxZoom()];
```

When `styleURL` is not a parseable URL (a JSON style body reliably is not — it contains literal spaces and unescaped `{`/`"`/`:` characters), `[NSURL URLWithString:styleURL]` returns `nil`. `MLNTilePyramidOfflineRegion` accepts a `nil` style URL and falls back to MapLibre's own default pseudo style URL (`maplibre://maps/style`). The region is then added and **persisted** to the on-device offline database, and — because `createPack:`'s completion handler calls `[pack resume]` immediately — its download state is left "active," so it auto-resumes on every later app launch.

On resume, `OfflineDownload::activateDownload()` fetches and processes the region's style. The default pseudo-URL resolves through `TileServerOptions::MapLibreConfiguration()` to MapLibre's own demo style config, whose `glyphs` field points at `demotiles.maplibre.org` — which is exactly the trigger #4403 already root-caused: canonicalising that glyphs URL builds an ECMAScript `std::regex` from a template with unescaped `{fontstack}`/`{start}`/`{end}` tokens, which throws `std::regex_error`, uncaught, on the `DatabaseFileSource` thread. `std::terminate` → `SIGABRT`. **Every launch, forever, with no application code able to intervene in time** — the crash fires before JS/native app code gets a chance to enumerate or delete the offending pack.

The Android module (`MLRNOfflineModule.kt`) has the same shape: `options.getString("mapStyle")` is forwarded straight into `OfflineTilePyramidRegionDefinition` with no URL validation.

## Impact

A single accidental `createPack` call with a non-URL `mapStyle` — trivially easy to hit if a style is composed in JS/Kotlin/Swift rather than fetched from one canonical URL, since both a URL and a serialised style are plain strings — permanently bricks the affected install: the app cannot survive long enough to run its own cleanup code, because the crash fires as soon as the persisted region auto-resumes and MapLibre requests its style. Deleting the poisoned row from the on-device sqlite database (`regions` table) with the app killed is currently the only way out we found; a from-JS repair has to run and complete before any ``/file-source activity can resume the poisoned download, which is delicate to get right blind.

## Reproduction

1. `OfflineManager.createPack({ mapStyle: JSON.stringify(someStyleObject), bounds, minZoom, maxZoom, metadata }, progressListener, errorListener)` — a JSON style body, not a URL.
2. `createPack` resolves successfully; no error is surfaced to JS at all.
3. Kill and relaunch the app with any live map. It crashes with #4403's `std::regex_error` on `org.maplibre.mbgl.DatabaseFileSource` — reliably, every launch.

## Suggested fix

Either (ideally both):

1. **Validate `mapStyle` at the native boundary** in both `createPack:` (iOS) and `createPack` (Android): reject with a clear `Promise` rejection when it does not parse as an absolute URL, rather than silently falling back to a default that later crashes the host app.
2. **Type it more precisely than `string`** in the TS surface (a branded/opaque type, or at minimum JSDoc that says "a style URL, never a style body") so the mistake is harder to make in the first place.

We worked around this entirely in our own app (never hand `createPack` anything but a validated URL, plus a boot-time repair that deletes any already-poisoned pack before any map can render) — happy to send a PR for the native-side validation if useful, since we already have the failure mode fully characterised on iOS.

## Related

- #4403 — the actual uncaught-exception trigger this reaches; the connection to `createPack` accepting a non-URL `mapStyle` in the first place doesn't appear to be documented anywhere, so filing separately.

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

Read MLRNOfflineModule.mm and MLRNOfflineModule.kt, then inspect the TypeScript OfflineManager surface and the createPack reproduction. Done means non-absolute or non-parseable mapStyle values are rejected clearly on both native platforms and no invalid pack is persisted or resumed.

Written by the indexing model from the issue text.

Assessment

Tech stack
kotlin, objective-c, react-native, typescript
Domain
api, mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.