Skip to content

tail() and reverse() copy a lazy range() into an ArrayList, so a big range exhausts the heap again #6353

Closed

Description

@lvca

Found while fixing #6323, which removed the same failure from the slice operator: range(0, 999999999)[0..1000000000] copied the slice into an ArrayList and exhausted the heap, and LongRangeList.subList already answered it in constant space. The slice was not the only copier.

tail() and reverse() copy the whole range

Both materialise, so with arcadedb.queryMaxRangeSize disabled (or set high) a single authenticated request allocates a billion boxed longs - tens of GB - which is advisory GHSA-xmjm-8q85-g778 in a second dress:

  • TailFunction.execute (engine/src/main/java/com/arcadedb/function/coll/TailFunction.java): new ArrayList<>(list.subList(1, list.size())). The copy is what costs; list.subList(1, size) on a LongRangeList is already a lazy LongRangeList, so the new ArrayList<>(...) around it is the whole bug.
  • ReverseFunction.execute (engine/src/main/java/com/arcadedb/function/misc/ReverseFunction.java): new ArrayList<>(list) followed by Collections.reverse.

Both have exact lazy answers, because a range reversed or beheaded is still an arithmetic progression:

  • tail(range) is range.subList(1, size) - already constant space, just do not copy it.
  • reverse(range) is new LongRangeList(start + (size - 1) * step, -step, size).

head(), last(), size() and indexing are already fine - they index rather than copy - and IN and slicing were fixed in #6344.

The coll.* family copies too

CollSort, CollRemove, CollFlatten and CollInsert each build an ArrayList of the whole input. Most of them genuinely need one - you cannot insert into a progression and get a progression - so the ask there is different: either bound the copy by arcadedb.queryMaxRangeSize and fail as a client error the way range() itself does, or answer lazily where the answer is still a range. sort(range) is one of those: an ascending range sorted is itself, and a descending one is its reverse.

What to check while in there

Whether any other consumer of a List argument copies it: the test to add is the shape CypherRangeHeapExhaustionTest already uses - call the function on range(0, 999999999) with the limit disabled, inside StallAwareStopwatch.assertStayedUnder, and let a copy fail as a timeout rather than as an OutOfMemoryError that takes the fork with it.

Related: #6323, #6344, advisory GHSA-xmjm-8q85-g778.

Activity

  1. self-assigned this
    on Aug 18, 2026
  2. added theissue type on Aug 18, 2026
  3. added this to the 26.9.1 milestone on Aug 18, 2026
  4. added a commit that references this issue on Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions