Reported by @ironpipp
Summary
traceSegment() in trace-mapping only bounds-checks the upper end of line (line >= decoded.length), not the lower end. When called with line: -1, it indexes decoded[-1], which is undefined in JS (not an error, just a nonexistent property on the array), and the subsequent memoizedBinarySearch(undefined, ...) crashes reading .length.
Its sibling public function originalPositionFor() does guard against this (if (line < 0) throw new Error(LINE_GTR_ZERO)), but traceSegment() — used internally by remapping()'s composition path — does not.
This is reachable from @jridgewell/remapping whenever a composed map's "youngest" map contains a real (non-sourceless) segment whose decoded sourceLine is -1. We hit this composing a Hermes-generated sourcemap (hermesc -output-source-map -O) with a bundler sourcemap in @expo/metro-config: Hermes occasionally emits -1 as sourceLine for optimizer-synthesized code, as a literal 4/5-length segment rather than the 1-length "sourceless" form remapping otherwise handles correctly.
Reproduction
import remapping from '@jridgewell/remapping';
const youngestMap = {
version: 3,
sources: ['bundle.js'],
names: [],
mappings: [[[0, 0, -1, -1]]], // line 0: [genCol=0, sourceIndex=0, sourceLine=-1, sourceColumn=-1]
};
const olderMap = {
version: 3,
sources: ['original.js'],
names: [],
mappings: [[[0, 0, 0, 0]]],
};
remapping([youngestMap, olderMap], () => null);
Actual behavior
TypeError: Cannot read properties of undefined (reading 'length')
at memoizedBinarySearch (trace-mapping.mjs:129:23)
at traceSegmentInternal (trace-mapping.mjs:433:15)
at traceSegment (trace-mapping.mjs:312:17)
at originalPositionFor (remapping.mjs:60:19)
at traceMappings (remapping.mjs:40:18)
at remapping (remapping.mjs:139:24)
Expected behavior
traceSegment() should treat a negative line the same way it treats a too-large one — return null (no mapping found) instead of throwing — mirroring the guard already present in originalPositionFor().
Environment
@jridgewell/remapping: 2.3.5
@jridgewell/trace-mapping: 0.3.31
- Node.js: 24.18.0, Windows
- Found via:
expo export --experimental-bundle --dump-sourcemap / eas update (Expo SDK 56, expo-updates 56.0.20, hermes-compiler 0.15.x win64 binary), composing Hermes's sourcemap with Metro's bundle sourcemap in @expo/metro-config's composeSourceMaps().
Reported by @ironpipp
Summary
traceSegment()intrace-mappingonly bounds-checks the upper end ofline(line >= decoded.length), not the lower end. When called withline: -1, it indexesdecoded[-1], which isundefinedin JS (not an error, just a nonexistent property on the array), and the subsequentmemoizedBinarySearch(undefined, ...)crashes reading.length.Its sibling public function
originalPositionFor()does guard against this (if (line < 0) throw new Error(LINE_GTR_ZERO)), buttraceSegment()— used internally byremapping()'s composition path — does not.This is reachable from
@jridgewell/remappingwhenever a composed map's "youngest" map contains a real (non-sourceless) segment whose decodedsourceLineis-1. We hit this composing a Hermes-generated sourcemap (hermesc -output-source-map -O) with a bundler sourcemap in@expo/metro-config: Hermes occasionally emits-1assourceLinefor optimizer-synthesized code, as a literal 4/5-length segment rather than the 1-length "sourceless" formremappingotherwise handles correctly.Reproduction
Actual behavior
Expected behavior
traceSegment()should treat a negativelinethe same way it treats a too-large one — returnnull(no mapping found) instead of throwing — mirroring the guard already present inoriginalPositionFor().Environment
@jridgewell/remapping: 2.3.5@jridgewell/trace-mapping: 0.3.31expo export --experimental-bundle --dump-sourcemap/eas update(Expo SDK 56,expo-updates56.0.20,hermes-compiler0.15.x win64 binary), composing Hermes's sourcemap with Metro's bundle sourcemap in@expo/metro-config'scomposeSourceMaps().