{"record":{"id":"d6e9ffdf7bb8f87d","repo":"apache/cassandra","slug":"a-range-supplied-to-sstablecursorreader-ends-befor","errorCode":null,"errorMessage":"A range supplied to SSTableCursorReader ends before it starts: ${lowerPosition} > ${upperPosition}","messagePattern":"A range supplied to SSTableCursorReader ends before it starts: (.+?) > (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"src/java/org/apache/cassandra/io/sstable/SSTableCursorReader.java","lineNumber":681,"sourceCode":"    {\n        return dataReader.getPosition() < segmentEnd ? PARTITION_START : advanceSegment();\n    }\n\n    /**\n     * Enters the next segment that has bytes, as {@code SSTableSimpleScanner.advanceRange} does,\n     * and leaves the reader at its first partition.\n     *\n     * @return PARTITION_START, or DONE when no segment is left\n     */\n    private int advanceSegment()\n    {\n        while (segmentIndex < segments.length)\n        {\n            PartitionPositionBounds next = segments[segmentIndex++];\n            if (segmentEnd > next.lowerPosition)\n                throw new IllegalArgumentException(\"Ranges supplied to SSTableCursorReader must be non-overlapping and in ascending order.\");\n            if (next.upperPosition < next.lowerPosition)\n                throw new IllegalArgumentException(\"A range supplied to SSTableCursorReader ends before it starts: \"\n                                                   + next.lowerPosition + \" > \" + next.upperPosition);\n            // An empty range carries no partition. Skip it WITHOUT touching segmentStart, segmentEnd\n            // or the byte accounting: bytesRead() is bytesReadInPreviousSegments plus the progress\n            // through the current segment, so moving those to a range the reader never visits makes\n            // the count go backwards. The scanner avoids this by seeking to the empty range's start;\n            // not seeking is cheaper and reads nothing outside a range this cursor covers.\n            if (next.lowerPosition == next.upperPosition)\n                continue;\n\n            bytesReadInPreviousSegments += segmentEnd - segmentStart;\n            segmentStart = next.lowerPosition;\n            segmentEnd = next.upperPosition;\n            try\n            {\n                seekPartition(segmentStart);\n            }\n            catch (IOException e)\n            {","sourceCodeStart":663,"sourceCodeEnd":699,"githubUrl":"https://github.com/apache/cassandra/blob/88fd0f6a0eaed8943f05ac9e8f947882b8ddc8f1/src/java/org/apache/cassandra/io/sstable/SSTableCursorReader.java#L663-L699","documentation":"SSTableCursorReader validates each byte-range segment as it advances: a PartitionPositionBounds whose upperPosition is smaller than its lowerPosition is nonsensical. The reader throws IllegalArgumentException naming both positions rather than producing garbage reads.","triggerScenarios":"Constructing SSTableCursorReader with a segments array containing an inverted PartitionPositionBounds (lowerPosition > upperPosition), typically produced by buggy bound computation.","commonSituations":"Buggy custom range slicing of sstables; deserializing corrupted partition bounds metadata; arithmetic errors computing end offsets (e.g. wrong length added to start).","solutions":["Fix the code computing upperPosition so it is always >= lowerPosition.","Filter out or normalize inverted/empty ranges before constructing the reader.","Pre-validate all PartitionPositionBounds before passing them to the reader.","If bounds come from persisted metadata, regenerate them by rescanning the sstable."],"exampleFix":"// before\nSSTableCursorReader reader = new SSTableCursorReader(..., rawSegments);\n// after\nList<PartitionPositionBounds> valid = rawSegments.stream()\n    .filter(b -> b.upperPosition >= b.lowerPosition)\n    .collect(Collectors.toList());\nSSTableCursorReader reader = new SSTableCursorReader(..., valid.toArray(new PartitionPositionBounds[0]));","handlingStrategy":"validation","validationCode":"Arrays.stream(bounds)\n    .filter(b -> b.upperPosition < b.lowerPosition)\n    .findAny()\n    .ifPresent(b -> { throw new IllegalArgumentException(\"inverted bound \" + b); });","typeGuard":null,"tryCatchPattern":"try {\n    reader = new SSTableCursorReader(..., bounds);\n} catch (IllegalArgumentException e) {\n    // sanitize bounds (drop/fix inverted entries) and retry\n}","preventionTips":["Compute end offsets as start + measured length, never independently.","Filter empty/inverted ranges at the producer.","Assert bounds invariants wherever PartitionPositionBounds values are generated."],"tags":["sstable","argument-validation","bounds"],"backgroundTag":"invalid-argument-value","analyzedSha":"88fd0f6a0eaed8943f05ac9e8f947882b8ddc8f1","analyzedAt":"2026-09-10T07:29:22.284Z","contentChangedAt":"2026-09-10T07:29:22.284Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}