Skip to content

Commit d65c96c

Browse files
committed
Reject malformed MVV regions before the stale-mark sweep
The up-front sweep introduced for issue #292 followed length fields with no alignment validation: on an MVV with a corrupt length field the traversal landed inside a value and unmark() cleared bit 15 of a payload byte — inside the region, before the first pass's guard could reject it. Hoist the verify() call that already runs on every prune and sweep only well-formed regions; a malformed region is left to the first pass's guard, which throws without writing anything.
1 parent 63ca4db commit d65c96c

2 files changed

Lines changed: 77 additions & 12 deletions

File tree

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,13 @@ else if (target[targetOffset] != TYPE_MVV_BYTE) {
381381
* </p>
382382
* <p>
383383
* This method leaves the byte array unchanged if any of its checked
384-
* Exceptions is thrown, with one exception: stale mark bits left on disk
385-
* by a prune interrupted before the issue #286 fix are cleared up front
386-
* regardless of outcome (issue #292). Mark bits are transient state
387-
* private to this method and invisible to the read paths, so clearing
388-
* them repairs the corruption without altering any version's value.
384+
* Exceptions is thrown, with one exception: on a well-formed MVV, stale
385+
* mark bits left on disk by a prune interrupted before the issue #286 fix
386+
* are cleared up front regardless of outcome (issue #292). Mark bits are
387+
* transient state private to this method and invisible to the read paths,
388+
* so clearing them repairs the corruption without altering any version's
389+
* value. A malformed MVV is never swept — it is rejected with a
390+
* <code>CorruptValueException</code> and left unchanged.
389391
* </p>
390392
* <p>
391393
* This method adds {@link PrunedVersion} instances to the supplied list.
@@ -424,7 +426,8 @@ static int prune(final byte[] bytes, final int offset, final int length, final T
424426
return length;
425427
}
426428

427-
Debug.$assert0.t(verify(bytes, offset, length));
429+
final boolean wellFormed = verify(bytes, offset, length);
430+
Debug.$assert0.t(wellFormed);
428431

429432
boolean primordial = convertToPrimordial;
430433
int marked = 0;
@@ -448,13 +451,21 @@ static int prune(final byte[] bytes, final int offset, final int length, final T
448451
* primordial-conversion scan — promoting an obsolete version and
449452
* silently dropping the current one with no PrunedVersion
450453
* accounting (issue #292). Sweep the region clean before the
451-
* first pass marks the real keepers.
454+
* first pass marks the real keepers. This is a separate pass, not
455+
* an unmark() at the first pass's loop head, so the whole region
456+
* is repaired even when that pass exits early via an exception.
457+
* Only a well-formed region may be swept: a corrupt length field
458+
* would misalign the traversal and unmark() would clear a bit
459+
* inside a value payload. On a malformed region skip the sweep —
460+
* the first pass's guard throws for it anyway.
452461
*/
453-
while (from + LENGTH_PER_VERSION <= offset + length) {
454-
unmark(bytes, from);
455-
from += getLength(bytes, from) + LENGTH_PER_VERSION;
462+
if (wellFormed) {
463+
int sweep = offset + 1;
464+
while (sweep + LENGTH_PER_VERSION <= offset + length) {
465+
unmark(bytes, sweep);
466+
sweep += getLength(bytes, sweep) + LENGTH_PER_VERSION;
467+
}
456468
}
457-
from = offset + 1;
458469
/*
459470
* First pass - mark all the versions to keep. Keep every
460471
* UNCOMMITTED version (there may be more than one created by the

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ public void pruneExceptionUnmarksVersionsAtNonZeroOffset() throws Exception {
586586
fail("expected CorruptValueException");
587587
} catch (final CorruptValueException expected) {
588588
}
589-
assertArrayEquals("prune must leave the byte array unchanged when it throws", before, bytes);
589+
assertArrayEquals("prune must leave an MVV without stale marks unchanged when it throws", before, bytes);
590590
}
591591

592592
/**
@@ -740,6 +740,44 @@ public void pruneStaleMarkedVersionPrunedInMultiVersionPath() throws Exception {
740740
}
741741
}
742742

743+
/**
744+
* The up-front sweep must not follow a corrupted length field: a
745+
* misaligned traversal lands inside a value and unmark() clears bit 15 of
746+
* a payload byte — inside the MVV region, past what
747+
* {@link #pruneExceptionMustNotUnmarkPastRegionEnd} guards. Prune must
748+
* reject the malformed region without writing anything.
749+
*/
750+
@Test
751+
public void pruneMustNotSweepMisalignedMvv() throws Exception {
752+
final TimestampAllocator tsa = new TimestampAllocator();
753+
final TransactionIndex ti = new TransactionIndex(tsa, 1);
754+
755+
/*
756+
* Two aborted versions, so no pass marks anything and any byte that
757+
* differs after prune was written by the sweep. The 0xFF fill of the
758+
* second value makes a misaligned unmark() visible (bit 15 cleared).
759+
*/
760+
final int offset = 7;
761+
final byte[] bytes = new byte[offset + 100];
762+
final byte[] v1 = { 0x1, 0x2, 0x3, 0x4 };
763+
final byte[] v2 = new byte[20];
764+
Arrays.fill(v2, (byte) 0xFF);
765+
int length = storeAbortedVersion(tsa, ti, bytes, offset, -1, v1);
766+
length = storeAbortedVersion(tsa, ti, bytes, offset, length, v2);
767+
ti.updateActiveTransactionCache();
768+
769+
/* Corrupt the first version's length field: it claims 9 bytes, not 4. */
770+
MVV.putLength(bytes, offset + 1, 9);
771+
772+
final byte[] before = bytes.clone();
773+
try {
774+
MVV.prune(bytes, offset, length, ti, true, new ArrayList<MVV.PrunedVersion>());
775+
fail("expected CorruptValueException");
776+
} catch (final CorruptValueException expected) {
777+
}
778+
assertArrayEquals("prune must not write into a misaligned MVV region", before, bytes);
779+
}
780+
743781
//
744782
// Test helper methods
745783
//
@@ -760,6 +798,22 @@ private static int storeCommittedVersion(final TimestampAllocator tsa, final Tra
760798
return newLength;
761799
}
762800

801+
/**
802+
* Store <code>value</code> as a new version created by a registered
803+
* transaction and abort it, so that {@link MVV#prune} sees an aborted
804+
* version and marks nothing.
805+
*/
806+
private static int storeAbortedVersion(final TimestampAllocator tsa, final TransactionIndex ti,
807+
final byte[] bytes, final int offset, final int length, final byte[] value) throws Exception {
808+
final TransactionStatus status = ti.registerTransaction();
809+
final int newLength = MVV.storeVersion(bytes, offset, length, bytes.length,
810+
TransactionIndex.ts2vh(status.getTs()), value, 0, value.length) & STORE_LENGTH_MASK;
811+
status.incrementMvvCount();
812+
status.abort();
813+
ti.notifyCompleted(status, tsa.updateTimestamp());
814+
return newLength;
815+
}
816+
763817
private static int writeArray(final byte[] array, final int... contents) {
764818
assert contents.length <= array.length : "Too many values for array";
765819
for (int i = 0; i < contents.length; ++i) {

0 commit comments

Comments
 (0)