Skip to content

Commit f0bc997

Browse files
committed
Merge commit from fork
fix(List): guard oversized bounds in setListBounds (cherry picked from commit a1a1ee4)
1 parent 8ac83f4 commit f0bc997

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

__tests__/List.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,55 @@ describe('List', () => {
913913
});
914914
});
915915

916+
// A List addresses its values through a 32-wide trie using signed 32-bit
917+
// bitwise math. Sizes/indices at or beyond 2 ** 30 used to overflow that math:
918+
// the level-raising loop spun forever, hanging an empty List and OOM-crashing
919+
// (SIGABRT) a populated one, while `setSize` silently wrapped large values.
920+
// All of these must now throw a clear, catchable RangeError.
921+
describe('rejects out-of-range sizes instead of hanging / crashing', () => {
922+
const tooBig = 2 ** 30;
923+
924+
it('throws (does not hang) when setting a too-large index on an empty List', () => {
925+
expect(() => List().set(tooBig, 'x')).toThrow(RangeError);
926+
});
927+
928+
it('throws (does not OOM-crash) when setting a too-large index on a populated List', () => {
929+
const list = List(arrayOfSize(64));
930+
expect(() => list.set(tooBig, 'x')).toThrow(RangeError);
931+
});
932+
933+
it('throws for a numeric-string index coming through a setIn key path', () => {
934+
const state = fromJS({ items: arrayOfSize(64) });
935+
expect(() => state.setIn(['items', '1073741824'], 'x')).toThrow(
936+
RangeError
937+
);
938+
});
939+
940+
it('throws on setSize beyond the max rather than silently truncating', () => {
941+
// Previously returned size 0 and size 5 respectively.
942+
expect(() => List([1, 2, 3]).setSize(2 ** 31)).toThrow(RangeError);
943+
expect(() => List([1, 2, 3]).setSize(2 ** 32 + 5)).toThrow(RangeError);
944+
});
945+
946+
it('still allows operations within the addressable range', () => {
947+
expect(List([1, 2, 3]).setSize(1500).size).toBe(1500);
948+
expect(List([1, 2, 3]).set(5, 'x').size).toBe(6);
949+
// Largest in-range size is accepted by the bounds math (sparse, no alloc).
950+
expect(List([1, 2, 3]).setSize(tooBig).size).toBe(tooBig);
951+
});
952+
953+
it('handles a large in-range negative index without hanging', () => {
954+
// Exercises the deep-tree origin-normalization path (the `2 ** exp`
955+
// fallback). Must terminate rather than spin forever; the existing values
956+
// are shifted to the end of the grown List.
957+
const result = List([1, 2, 3]).set(-(2 ** 29), 'x');
958+
expect(result.size).toBe(2 ** 29);
959+
expect(result.get(result.size - 3)).toBe(1);
960+
expect(result.get(result.size - 2)).toBe(2);
961+
expect(result.get(result.size - 1)).toBe(3);
962+
});
963+
});
964+
916965
it('Does not infinite loop when sliced with NaN #459', () => {
917966
const list = List([1, 2, 3, 4, 5]);
918967
const newList = list.slice(0, NaN);

src/List.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,39 @@ function listNodeFor(list, rawIndex) {
526526
}
527527
}
528528

529+
/**
530+
* Validates requested bounds before int32 coercion in setListBounds().
531+
* Throws when origin/capacity would exceed the trie's safe range.
532+
*/
533+
function validateListBoundsRequest(list, begin, end) {
534+
const requestedOrigin = list._origin + (begin === undefined ? 0 : begin);
535+
const requestedCapacity =
536+
end === undefined
537+
? list._capacity
538+
: end < 0
539+
? list._capacity + end
540+
: list._origin + end;
541+
542+
// Keep origin/capacity within the trie's safe signed 32-bit range.
543+
if (
544+
(Number.isFinite(requestedCapacity) && requestedCapacity > MAX_LIST_SIZE) ||
545+
(Number.isFinite(requestedOrigin) && requestedOrigin < -MAX_LIST_SIZE) ||
546+
(Number.isFinite(requestedCapacity) &&
547+
Number.isFinite(requestedOrigin) &&
548+
requestedCapacity - requestedOrigin > MAX_LIST_SIZE)
549+
) {
550+
throw new RangeError(
551+
'Invalid List size: a List cannot hold more than ' +
552+
MAX_LIST_SIZE +
553+
' (2 ** 30) values.'
554+
);
555+
}
556+
}
557+
529558
function setListBounds(list, begin, end) {
559+
// Validate full-precision bounds before int32 coercion.
560+
validateListBoundsRequest(list, begin, end);
561+
530562
// Sanitize begin & end using this shorthand for ToInt32(argument)
531563
// http://www.ecma-international.org/ecma-262/6.0/#sec-toint32
532564
if (begin !== undefined) {
@@ -565,7 +597,8 @@ function setListBounds(list, begin, end) {
565597
owner
566598
);
567599
newLevel += SHIFT;
568-
offsetShift += 1 << newLevel;
600+
// Shift origin into non-negative space as trie height grows.
601+
offsetShift += levelCapacity(newLevel);
569602
}
570603
if (offsetShift) {
571604
newOrigin += offsetShift;
@@ -578,7 +611,7 @@ function setListBounds(list, begin, end) {
578611
const newTailOffset = getTailOffset(newCapacity);
579612

580613
// New size might need creating a higher root.
581-
while (newTailOffset >= 1 << (newLevel + SHIFT)) {
614+
while (newTailOffset >= levelCapacity(newLevel + SHIFT)) {
582615
newRoot = new VNode(
583616
newRoot && newRoot.array.length ? [newRoot] : [],
584617
owner
@@ -675,3 +708,16 @@ function setListBounds(list, begin, end) {
675708
function getTailOffset(size) {
676709
return size < SIZE ? 0 : ((size - 1) >>> SHIFT) << SHIFT;
677710
}
711+
712+
// The largest number of values a List can hold. Above this the 32-bit trie math
713+
// in setListBounds() stays in the safe signed 32-bit range.
714+
const MAX_LIST_SIZE = 2 ** 30; // 1073741824
715+
716+
/**
717+
* Computes 2 ** exp for the trie level-raising loops in setListBounds().
718+
* Use the cheap bitwise operator shift whenever possible, otherwise fall back to exponentiation.
719+
* This is necessary because bitwise operators in JavaScript only work on 32-bit signed integers, so for exp >= 31, we need to use exponentiation to avoid overflow.
720+
*/
721+
function levelCapacity(exp) {
722+
return exp < 31 ? 1 << exp : 2 ** exp;
723+
}

0 commit comments

Comments
 (0)