maplibre / maplibre/maplibre-react-native
createPack silently accepts a non-URL mapStyle and persists a pack that crashes the app on every later launch
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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