Skip to content

Commit d6ce5c9

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 - same advance, same guard, with the array-end bound covering the unguarded first iteration - 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 d6ce5c9

3 files changed

Lines changed: 231 additions & 26 deletions

File tree

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

Lines changed: 59 additions & 17 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
}
@@ -578,17 +584,21 @@ static int prune(final byte[] bytes, final int offset, final int length, final T
578584
throw new PersistitInterruptedException(ie);
579585
} finally {
580586
/*
581-
* Make sure all marks are removed even if this method exits via an
582-
* Exception.
587+
* Remove marks left by pass 1 even when this method exits via an
588+
* exception: a mark left in a live page is read back as part of
589+
* the version length and corrupts the MVV (issue #286). Mirrors
590+
* pass 1 - same advance, same guard - so it touches exactly the
591+
* indices that pass could have marked, and nothing else. A
592+
* zero-length version is legal, hence no break on vlength == 0.
593+
* The array-end bound covers pass 1's unguarded first iteration.
583594
*/
584595
if (marked > 0) {
585596
int index = offset + 1;
586-
while (index < length) {
597+
while (index + LENGTH_PER_VERSION <= bytes.length) {
587598
final int vlength = getLength(bytes, index);
588-
Debug.$assert0.t(vlength + index + LENGTH_PER_VERSION <= offset + length);
589599
unmark(bytes, index);
590600
index += vlength + LENGTH_PER_VERSION;
591-
if (vlength <= 0) {
601+
if (index + LENGTH_PER_VERSION > offset + length) {
592602
break;
593603
}
594604
}
@@ -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
}

0 commit comments

Comments
 (0)