Skip to content

Commit cd75433

Browse files
jridgewellcodebytere
authored andcommitted
process: fix two overflow cases in SourceMap VLQ decoding
These both have to do with extremely large numbers, so it's unlikely to cause a problem in practice. Still, correctness. First, encoding `-2147483648` in VLQ returns the value `"B"`. When decoding, we get the value `1` after reading the base64. We then check if the first bit is set (it is) to see if we should negate it, then we shift all bits right once. Now, `value` will be `0` and `negate` will be `true`. So, we'd return `-0`. Which is a bug! `-0` isn't `-2147483648`, and we've broken a round trip. Second, encoding any number with the 31st bit set, we'd return the opposite sign. Let's use `1073741824`. Encoding, we get `"ggggggC"`. When decoding, we get the value `-2147483648` after reading the base64. Notice, it's already negative (the 32nd bit is set, because the 31st was set and we shifted everything left once). We'd then check the first bit (it's not) and shift right. But we used `>>`, which does not shift the sign bit. We actually wanted `>>>`, which will. Because of that bug, we get back `-1073741824` instead of the positive `1073741824`. It's even worse if the 32nd and 31st bits are set, `-1610612736` becomes `536870912` after a round trip. I recently fixed the same two bugs in Closure Compiler: google/closure-compiler@584418eb PR-URL: #31490 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Ben Coe <bencoe@gmail.com> Reviewed-By: David Carlier <devnexen@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Gus Caplan <me@gus.host>
1 parent 9d45393 commit cd75433

File tree

2 files changed

+55
-2
lines changed

2 files changed

+55
-2
lines changed

lib/internal/source_map/source_map.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,19 @@ function decodeVLQ(stringCharIterator) {
303303

304304
// Fix the sign.
305305
const negative = result & 1;
306-
result >>= 1;
307-
return negative ? -result : result;
306+
// Use unsigned right shift, so that the 32nd bit is properly shifted to the
307+
// 31st, and the 32nd becomes unset.
308+
result >>>= 1;
309+
if (!negative) {
310+
return result;
311+
}
312+
313+
// We need to OR here to ensure the 32nd bit (the sign bit in an Int32) is
314+
// always set for negative numbers. If `result` were 1, (meaning `negate` is
315+
// true and all other bits were zeros), `result` would now be 0. But -0
316+
// doesn't flip the 32nd bit as intended. All other numbers will successfully
317+
// set the 32nd bit without issue, so doing this is a noop for them.
318+
return -result | (1 << 31);
308319
}
309320

310321
/**

test/parallel/test-source-map-api.js

+42
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,45 @@ const { readFileSync } = require('fs');
8282
assert.strictEqual(payload.sources[0], sourceMap.payload.sources[0]);
8383
assert.notStrictEqual(payload.sources, sourceMap.payload.sources);
8484
}
85+
86+
// Test various known decodings to ensure decodeVLQ works correctly.
87+
{
88+
function makeMinimalMap(column) {
89+
return {
90+
sources: ['test.js'],
91+
// Mapping from the 0th line, 0th column of the output file to the 0th
92+
// source file, 0th line, ${column}th column.
93+
mappings: `AAA${column}`,
94+
};
95+
}
96+
const knownDecodings = {
97+
'A': 0,
98+
'B': -2147483648,
99+
'C': 1,
100+
'D': -1,
101+
'E': 2,
102+
'F': -2,
103+
104+
// 2^31 - 1, maximum values
105+
'+/////D': 2147483647,
106+
'8/////D': 2147483646,
107+
'6/////D': 2147483645,
108+
'4/////D': 2147483644,
109+
'2/////D': 2147483643,
110+
'0/////D': 2147483642,
111+
112+
// -2^31 + 1, minimum values
113+
'//////D': -2147483647,
114+
'9/////D': -2147483646,
115+
'7/////D': -2147483645,
116+
'5/////D': -2147483644,
117+
'3/////D': -2147483643,
118+
'1/////D': -2147483642,
119+
};
120+
121+
for (const column in knownDecodings) {
122+
const sourceMap = new SourceMap(makeMinimalMap(column));
123+
const { originalColumn } = sourceMap.findEntry(0, 0);
124+
assert.strictEqual(originalColumn, knownDecodings[column]);
125+
}
126+
}

0 commit comments

Comments
 (0)