Skip to content

traceSegment() in trace-mapping doesn't guard against negative line, crashing remapping() when a source map segment has sourceLine: -1 #54

Description

@ironpipp

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().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions