Skip to content

Commit b669a2f

Browse files
committed
Fix MVV marked-version corruption after interrupted prune
MVV.prune's finally-block safety net used the wrong loop bound (index < length instead of index < offset + length), so a prune that exited via an exception - e.g. PersistitInterruptedException while resolving a commit status under interrupt - left mark bits set in the live buffer page whenever the MVV sat at a non-zero in-page offset. The fetch paths (visitAllVersions, fetchVersion, fetchVersionByOffset) then read the version length signed and with the mark bit included, driving the scan offset negative and crashing with ArrayIndexOutOfBoundsException, as seen in TransactionTest2.transactionsWithInterrupts on Windows JDK 11. - Make the finally-block unmark loop in MVV.prune mirror the first pass exactly (bounded only by the array end, past which mark() cannot have written) and remove its early break on zero-length versions (a legal undefined value) - Tighten the first pass's corruption guard so a misaligned traversal throws CorruptValueException before it can read or mark outside the MVV region - Read version lengths unsigned with the mark bit stripped in visitAllVersions, fetchVersion and fetchVersionByOffset - Throw CorruptValueException instead of AIOOBE when a version header or value extends past the MVV bounds - Fix storeVersion reading target[0] instead of target[targetOffset] and exactRequiredLength's loop bound ignoring targetOffset when the MVV sits at a non-zero offset - Add deterministic regression tests for marked-version reads and for the interrupted-prune unmark safety net; make transactionsWithInterrupts assert no worker died with an unexpected exception, reporting the first failure, with shared counters reset in @before Fixes #286
1 parent 0f5e380 commit b669a2f

3 files changed

Lines changed: 195 additions & 27 deletions

File tree

persistit/core/src/main/java/com/persistit/MVV.java

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ static int exactRequiredLength(final byte[] target, final int targetOffset, fina
171171
return overheadLength(2) + targetLength + newVersionLength;
172172
} else {
173173
int offset = targetOffset + 1;
174-
while (offset < targetLength) {
174+
while (offset < targetOffset + targetLength) {
175175
final long version = getVersion(target, offset);
176176
final int valueLength = getLength(target, offset);
177177
offset += LENGTH_PER_VERSION + valueLength;
@@ -295,7 +295,7 @@ else if (targetLength == 0) {
295295
* Value previously existed as a primordial value. Result will be an MVV
296296
* with two versions.
297297
*/
298-
else if (target[0] != TYPE_MVV_BYTE) {
298+
else if (target[targetOffset] != TYPE_MVV_BYTE) {
299299
assertCapacity(targetLimit, targetOffset + sourceLength + targetLength + overheadLength(2));
300300
// Promote to MVV, shift existing down for header
301301
System.arraycopy(target, to, target, to + LENGTH_TYPE_MVV + LENGTH_PER_VERSION, targetLength);
@@ -484,7 +484,13 @@ static int prune(final byte[] bytes, final int offset, final int length, final T
484484
}
485485
}
486486
from += vlength + LENGTH_PER_VERSION;
487-
if (from > offset + length) {
487+
if (from != offset + length && from + LENGTH_PER_VERSION > offset + length) {
488+
/*
489+
* A version must either end exactly at the region end or
490+
* leave room for at least one more version header. Anything
491+
* else is a misaligned traversal; reject it before the next
492+
* iteration reads or marks outside the MVV region.
493+
*/
488494
throw new CorruptValueException("MVV Value is corrupt at index: " + from);
489495
}
490496
}
@@ -579,18 +585,22 @@ static int prune(final byte[] bytes, final int offset, final int length, final T
579585
} finally {
580586
/*
581587
* Make sure all marks are removed even if this method exits via an
582-
* Exception.
588+
* Exception. Marks left behind in a live buffer page are read back
589+
* later as part of the version length and corrupt the MVV (see
590+
* issue #286). A zero-length version is legal (undefined value),
591+
* so the loop must not stop on vlength == 0; it always advances by
592+
* at least LENGTH_PER_VERSION. The iteration must mirror the first
593+
* pass exactly — on a corrupt MVV that pass can visit a version
594+
* starting inside the trailing LENGTH_PER_VERSION bytes, so this
595+
* loop may not stop earlier; the only hard bound is the array end,
596+
* past which mark() could not have written.
583597
*/
584598
if (marked > 0) {
585599
int index = offset + 1;
586-
while (index < length) {
600+
while (index < offset + length && index + LENGTH_PER_VERSION <= bytes.length) {
587601
final int vlength = getLength(bytes, index);
588-
Debug.$assert0.t(vlength + index + LENGTH_PER_VERSION <= offset + length);
589602
unmark(bytes, index);
590603
index += vlength + LENGTH_PER_VERSION;
591-
if (vlength <= 0) {
592-
break;
593-
}
594604
}
595605
}
596606
}
@@ -663,8 +673,12 @@ static boolean verify(final TransactionIndex ti, final byte[] bytes, final int o
663673
* if no value was copied.
664674
* @throws IllegalArgumentException
665675
* If the target array is too small to hold the value
676+
* @throws CorruptValueException
677+
* If a version header or value extends past
678+
* <code>sourceLength</code>
666679
*/
667-
public static int fetchVersion(final byte[] source, final int sourceLength, final long version, final byte[] target) {
680+
public static int fetchVersion(final byte[] source, final int sourceLength, final long version, final byte[] target)
681+
throws CorruptValueException {
668682
int offset = 0;
669683
int length = VERSION_NOT_FOUND;
670684

@@ -673,9 +687,17 @@ public static int fetchVersion(final byte[] source, final int sourceLength, fina
673687
} else if (sourceLength > 0 && source[0] == TYPE_MVV_BYTE) {
674688
offset = 1;
675689
while (offset < sourceLength) {
676-
final long curVersion = Util.getLong(source, offset);
677-
final int curLength = Util.getShort(source, offset + LENGTH_VERSION);
690+
if (offset + LENGTH_PER_VERSION > sourceLength) {
691+
throw new CorruptValueException("MVV version header at offset " + offset
692+
+ " extends past length " + sourceLength);
693+
}
694+
final long curVersion = getVersion(source, offset);
695+
final int curLength = getLength(source, offset);
678696
offset += LENGTH_PER_VERSION;
697+
if (curLength > sourceLength - offset) {
698+
throw new CorruptValueException("MVV version value at offset " + offset + " with length "
699+
+ curLength + " extends past length " + sourceLength);
700+
}
679701
if (curVersion == version) {
680702
length = curLength;
681703
break;
@@ -739,15 +761,29 @@ public static void visitAllVersions(final VersionVisitor visitor, final byte[] s
739761
} else if (source[sourceOffset] != TYPE_MVV_BYTE) {
740762
visitor.sawVersion(PRIMORDIAL_VALUE_VERSION, sourceOffset, sourceLength);
741763
} else {
764+
final int end = sourceOffset + sourceLength;
742765
int offset = sourceOffset + 1;
743-
while (offset < sourceOffset + sourceLength) {
744-
final long version = Util.getLong(source, offset);
745-
final int valueLength = Util.getShort(source, offset + LENGTH_VERSION);
766+
while (offset < end) {
767+
if (offset + LENGTH_PER_VERSION > end) {
768+
throw new CorruptValueException("MVV version header at offset " + offset
769+
+ " extends past offset/length=" + sourceOffset + "/" + sourceLength);
770+
}
771+
final long version = getVersion(source, offset);
772+
final int valueLength = getLength(source, offset);
746773
offset += LENGTH_PER_VERSION;
774+
if (valueLength > end - offset) {
775+
throw new CorruptValueException("MVV version value at offset " + offset + " with length "
776+
+ valueLength + " extends past offset/length=" + sourceOffset + "/" + sourceLength);
777+
}
747778
visitor.sawVersion(version, offset, valueLength);
748779
offset += valueLength;
749780
}
750-
if (offset != sourceOffset + sourceLength) {
781+
/*
782+
* Defensive only: the per-version guards above make this
783+
* unreachable (the loop can only exit with offset == end). Kept in
784+
* case the traversal logic changes.
785+
*/
786+
if (offset != end) {
751787
throw new CorruptValueException("invalid length in MVV at offset/length=" + sourceOffset + "/"
752788
+ sourceLength);
753789
}
@@ -773,14 +809,20 @@ public static void visitAllVersions(final VersionVisitor visitor, final byte[] s
773809
* if no value was copied.
774810
* @throws IllegalArgumentException
775811
* If the target array is too small to hold the value
812+
* @throws CorruptValueException
813+
* If the stored value extends past <code>sourceLength</code>
776814
*/
777815
public static int fetchVersionByOffset(final byte[] source, final int sourceLength, final int offset,
778-
final byte[] target) {
816+
final byte[] target) throws CorruptValueException {
779817
if (offset < 0 || (offset > 0 && offset > sourceLength)) {
780818
throw new IllegalArgumentException("Offset out of range: " + offset);
781819
}
782-
final int length = (offset == 0) ? sourceLength : Util.getShort(source, offset - LENGTH_VALUE_LENGTH);
820+
final int length = (offset == 0) ? sourceLength : getLength(source, offset - LENGTH_PER_VERSION);
783821
if (length > 0) {
822+
if (length > sourceLength - offset) {
823+
throw new CorruptValueException("MVV version value at offset " + offset + " with length " + length
824+
+ " extends past length " + sourceLength);
825+
}
784826
assertCapacity(target.length, length);
785827
System.arraycopy(source, offset, target, 0, length);
786828
}

persistit/core/src/test/java/com/persistit/MVVTest.java

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@
1212
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
15+
* Portions Copyrighted 2026 3A Systems, LLC
1516
*/
1617

1718
package com.persistit;
1819

20+
import com.persistit.exception.CorruptValueException;
1921
import com.persistit.exception.PersistitException;
2022
import com.persistit.util.Util;
2123
import junit.framework.Assert;
2224
import org.junit.Test;
2325

26+
import java.util.ArrayList;
2427
import java.util.Map;
2528
import java.util.TreeMap;
2629

@@ -30,6 +33,7 @@
3033
import static org.junit.Assert.assertArrayEquals;
3134
import static org.junit.Assert.assertEquals;
3235
import static org.junit.Assert.assertTrue;
36+
import static org.junit.Assert.fail;
3337

3438
public class MVVTest {
3539
@Test
@@ -321,31 +325,31 @@ public void storeToExistingVersionAtCapacityShorterLength() {
321325
}
322326

323327
@Test
324-
public void fetchVersionFromUnused() {
328+
public void fetchVersionFromUnused() throws PersistitException {
325329
final long vh = 10;
326330
final byte[] source = {};
327331
final byte[] target = {};
328332
assertEquals(MVV.VERSION_NOT_FOUND, MVV.fetchVersion(source, -1, vh, target));
329333
}
330334

331335
@Test
332-
public void fetchVersionFromUndefined() {
336+
public void fetchVersionFromUndefined() throws PersistitException {
333337
final long vh = 10;
334338
final byte[] source = {};
335339
final byte[] target = {};
336340
assertEquals(MVV.VERSION_NOT_FOUND, MVV.fetchVersion(source, source.length, vh, target));
337341
}
338342

339343
@Test
340-
public void fetchVersionFromPrimordial() {
344+
public void fetchVersionFromPrimordial() throws PersistitException {
341345
final long vh = 10;
342346
final byte[] source = { 0xA, 0xB, 0xC };
343347
final byte[] target = {};
344348
assertEquals(MVV.VERSION_NOT_FOUND, MVV.fetchVersion(source, source.length, vh, target));
345349
}
346350

347351
@Test
348-
public void fetchVersionFromExistingNoFound() {
352+
public void fetchVersionFromExistingNoFound() throws PersistitException {
349353
final long vh = 10;
350354
final byte[] source = { (byte) TYPE_MVV, 0, 0, 0, 0, 0, 0, 0, 1, 0, 3, 0xA, 0xB, 0xC, 0, 0, 0, 0, 0, 0, 0, 2,
351355
0, 2, 0xD, 0xE };
@@ -354,7 +358,7 @@ public void fetchVersionFromExistingNoFound() {
354358
}
355359

356360
@Test
357-
public void fetchVersionFromExisting() {
361+
public void fetchVersionFromExisting() throws PersistitException {
358362
final long vh = 10;
359363
final byte[] source = { (byte) TYPE_MVV, 0, 0, 0, 0, 0, 0, 0, 10, 0, 2, 0xA, 0xB, 0, 0, 0, 0, 0, 0, 0, 11, 0,
360364
3, 0xB, 0xC };
@@ -366,7 +370,7 @@ public void fetchVersionFromExisting() {
366370
}
367371

368372
@Test(expected = IllegalArgumentException.class)
369-
public void fetchVersionFromExistingOverCapacity() {
373+
public void fetchVersionFromExistingOverCapacity() throws PersistitException {
370374
final long vh = 10;
371375
final byte[] source = { (byte) TYPE_MVV, 0, 0, 0, 0, 0, 0, 0, 11, 0, 5, 0x1, 0x2, 0x3, 0x4, 0x5, 0, 0, 0, 0, 0,
372376
0, 0, 9, 0, 1, 0xA, 0, 0, 0, 0, 0, 0, 0, 10, 0, 3, 0xB, 0xC, 0xD };
@@ -429,21 +433,21 @@ public void visitAndFetchByOffsetMVV() throws PersistitException {
429433
}
430434

431435
@Test(expected = IllegalArgumentException.class)
432-
public void fetchByOffsetNegative() {
436+
public void fetchByOffsetNegative() throws PersistitException {
433437
final byte[] source = {};
434438
final byte[] target = new byte[10];
435439
MVV.fetchVersionByOffset(source, source.length, -1, target);
436440
}
437441

438442
@Test(expected = IllegalArgumentException.class)
439-
public void fetchByOffsetTooLarge() {
443+
public void fetchByOffsetTooLarge() throws PersistitException {
440444
final byte[] source = newArray(TYPE_MVV, 0, 0, 0, 0, 0, 0, 0, 1, 0, 3, 0xA, 0xB, 0xC);
441445
final byte[] target = new byte[10];
442446
MVV.fetchVersionByOffset(source, source.length, source.length + 1, target);
443447
}
444448

445449
@Test
446-
public void storeAndFetchVersionMany() {
450+
public void storeAndFetchVersionMany() throws PersistitException {
447451
final int VERSION_COUNT = 10;
448452
final int versions[] = new int[VERSION_COUNT];
449453
final byte sources[][] = new byte[VERSION_COUNT][];
@@ -480,6 +484,109 @@ public void tryValueTooLong() {
480484
MVV.storeVersion(target, 0, 0, target.length, VERSION, source, 0, source.length);
481485
}
482486

487+
//
488+
// Issue #286: a version left in the marked state (e.g. by a prune that was
489+
// interrupted mid-way) must still be readable. The length accessors used
490+
// by the fetch paths formerly read the length field signed and with the
491+
// mark bit included, driving the scan offset negative and throwing
492+
// ArrayIndexOutOfBoundsException.
493+
//
494+
495+
@Test
496+
public void visitMarkedVersion() throws PersistitException {
497+
final byte[] source = newArray(TYPE_MVV, 0, 0, 0, 0, 0, 0, 0, 1, 0, 3, 0xA, 0xB, 0xC, 0, 0, 0, 0, 0, 0, 0, 2,
498+
0, 2, 0xD, 0xE);
499+
MVV.mark(source, 1);
500+
final TestVisitor visitor = new TestVisitor();
501+
MVV.visitAllVersions(visitor, source, 0, source.length);
502+
assertTrue(visitor.initCalled);
503+
assertEquals(newVisitorMap(1, 3, 11, 2, 2, 24), visitor.versions);
504+
}
505+
506+
@Test
507+
public void fetchMarkedVersion() throws PersistitException {
508+
final byte[] source = newArray(TYPE_MVV, 0, 0, 0, 0, 0, 0, 0, 10, 0, 2, 0xA, 0xB, 0, 0, 0, 0, 0, 0, 0, 11, 0,
509+
3, 0xC, 0xD, 0xE);
510+
MVV.mark(source, 1);
511+
MVV.mark(source, 13);
512+
final byte[] target = new byte[20];
513+
assertEquals(2, MVV.fetchVersion(source, source.length, 10, target));
514+
assertArrayEqualsLen(newArray(0xA, 0xB), target, 2);
515+
assertEquals(3, MVV.fetchVersion(source, source.length, 11, target));
516+
assertArrayEqualsLen(newArray(0xC, 0xD, 0xE), target, 3);
517+
}
518+
519+
@Test
520+
public void fetchByOffsetMarkedVersion() throws PersistitException {
521+
final byte[] source = newArray(TYPE_MVV, 0, 0, 0, 0, 0, 0, 0, 10, 0, 2, 0xA, 0xB, 0, 0, 0, 0, 0, 0, 0, 11, 0,
522+
3, 0xC, 0xD, 0xE);
523+
MVV.mark(source, 1);
524+
final byte[] target = new byte[20];
525+
assertEquals(2, MVV.fetchVersionByOffset(source, source.length, 11, target));
526+
assertArrayEqualsLen(newArray(0xA, 0xB), target, 2);
527+
}
528+
529+
@Test(expected = CorruptValueException.class)
530+
public void visitTruncatedHeaderThrows() throws PersistitException {
531+
final byte[] source = newArray(TYPE_MVV, 0, 0, 0, 0, 0);
532+
MVV.visitAllVersions(new TestVisitor(), source, 0, source.length);
533+
}
534+
535+
/**
536+
* The old code also threw CorruptValueException here (from the trailing
537+
* length check), but only after handing the visitor an out-of-bounds
538+
* version — which Exchange.MvvVisitor would record and fetchVersionByOffset
539+
* would then copy. The fix must throw before the visitor sees it.
540+
*/
541+
@Test
542+
public void visitOverrunningLengthThrows() throws PersistitException {
543+
final byte[] source = newArray(TYPE_MVV, 0, 0, 0, 0, 0, 0, 0, 1, 0, 50, 0xA, 0xB, 0xC);
544+
final TestVisitor visitor = new TestVisitor();
545+
try {
546+
MVV.visitAllVersions(visitor, source, 0, source.length);
547+
fail("expected CorruptValueException");
548+
} catch (final CorruptValueException expected) {
549+
}
550+
assertTrue("visitor must not see an out-of-bounds version", visitor.versions.isEmpty());
551+
}
552+
553+
/**
554+
* Issue #286: when prune exits via an exception after its first pass has
555+
* marked versions (e.g. interrupted while resolving a commit status, or a
556+
* CorruptValueException), the finally-block safety net must remove the
557+
* marks. Its loop bound was wrong for a non-zero offset — exactly the
558+
* in-page pruning case — leaving mark bits behind in the live buffer page.
559+
*/
560+
@Test
561+
public void pruneExceptionUnmarksVersionsAtNonZeroOffset() throws Exception {
562+
final TimestampAllocator tsa = new TimestampAllocator();
563+
final TransactionIndex ti = new TransactionIndex(tsa, 1);
564+
final TransactionStatus status1 = ti.registerTransaction();
565+
final TransactionStatus status2 = ti.registerTransaction();
566+
567+
/*
568+
* Two uncommitted versions from different transactions make prune's
569+
* first pass throw "Multiple uncommitted versions" after it has marked
570+
* the first version. The non-zero offset emulates in-page pruning.
571+
*/
572+
final int offset = 117;
573+
final byte[] bytes = new byte[offset + 100];
574+
final byte[] v1 = { 0xA, 0xB, 0xC };
575+
final byte[] v2 = { 0xD, 0xE };
576+
int length = MVV.storeVersion(bytes, offset, -1, bytes.length, TransactionIndex.ts2vh(status1.getTs()), v1, 0,
577+
v1.length) & STORE_LENGTH_MASK;
578+
length = MVV.storeVersion(bytes, offset, length, bytes.length, TransactionIndex.ts2vh(status2.getTs()), v2, 0,
579+
v2.length) & STORE_LENGTH_MASK;
580+
581+
final byte[] before = bytes.clone();
582+
try {
583+
MVV.prune(bytes, offset, length, ti, true, new ArrayList<MVV.PrunedVersion>());
584+
fail("expected CorruptValueException");
585+
} catch (final CorruptValueException expected) {
586+
}
587+
assertArrayEquals("prune must leave the byte array unchanged when it throws", before, bytes);
588+
}
589+
483590
//
484591
// Test helper methods
485592
//

0 commit comments

Comments
 (0)