Skip to content

Commit 3ddadcf

Browse files
authored
HBASE-28065 Corrupt HFile data is mishandled in several cases
* when no block size is provided and there's not a preread headerBuf, treat the value with caution. * verify HBase checksums before making use of the block header. * inline verifyOnDiskSizeMatchesHeader to keep throw/return logic in the method body. * separate validation of onDiskSizeWithHeader as input parameter from as read from block header * simplify branching around fetching and populating onDiskSizeWithHeader. * inline retrieving nextOnDiskBlockSize ; add basic validation. * whenever a read is determined to be corrupt and fallback to HDFS checksum is necessary, also invalidate the cached value of headerBuf. * build out a test suite covering various forms of block header corruption, for blocks in first and second positions. Signed-off-by: Bryan Beaudreault <[email protected]>
1 parent dad1f70 commit 3ddadcf

File tree

4 files changed

+640
-52
lines changed

4 files changed

+640
-52
lines changed

hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/HFileBlock.java

Lines changed: 109 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -392,12 +392,12 @@ static HFileBlock createFromBuff(ByteBuff buf, boolean usesHBaseChecksum, final
392392

393393
/**
394394
* Parse total on disk size including header and checksum.
395-
* @param headerBuf Header ByteBuffer. Presumed exact size of header.
396-
* @param verifyChecksum true if checksum verification is in use.
395+
* @param headerBuf Header ByteBuffer. Presumed exact size of header.
396+
* @param checksumSupport true if checksum verification is in use.
397397
* @return Size of the block with header included.
398398
*/
399-
private static int getOnDiskSizeWithHeader(final ByteBuff headerBuf, boolean verifyChecksum) {
400-
return headerBuf.getInt(Header.ON_DISK_SIZE_WITHOUT_HEADER_INDEX) + headerSize(verifyChecksum);
399+
private static int getOnDiskSizeWithHeader(final ByteBuff headerBuf, boolean checksumSupport) {
400+
return headerBuf.getInt(Header.ON_DISK_SIZE_WITHOUT_HEADER_INDEX) + headerSize(checksumSupport);
401401
}
402402

403403
/**
@@ -1597,33 +1597,48 @@ public HFileBlock readBlockData(long offset, long onDiskSizeWithHeaderL, boolean
15971597
}
15981598

15991599
/**
1600-
* Returns Check <code>onDiskSizeWithHeaderL</code> size is healthy and then return it as an int
1600+
* Check that {@code value} read from a block header seems reasonable, within a large margin of
1601+
* error.
1602+
* @return {@code true} if the value is safe to proceed, {@code false} otherwise.
16011603
*/
1602-
private static int checkAndGetSizeAsInt(final long onDiskSizeWithHeaderL, final int hdrSize)
1603-
throws IOException {
1604-
if (
1605-
(onDiskSizeWithHeaderL < hdrSize && onDiskSizeWithHeaderL != -1)
1606-
|| onDiskSizeWithHeaderL >= Integer.MAX_VALUE
1607-
) {
1608-
throw new IOException(
1609-
"Invalid onDisksize=" + onDiskSizeWithHeaderL + ": expected to be at least " + hdrSize
1610-
+ " and at most " + Integer.MAX_VALUE + ", or -1");
1604+
private boolean checkOnDiskSizeWithHeader(int value) {
1605+
if (value < 0) {
1606+
if (LOG.isTraceEnabled()) {
1607+
LOG.trace(
1608+
"onDiskSizeWithHeader={}; value represents a size, so it should never be negative.",
1609+
value);
1610+
}
1611+
return false;
1612+
}
1613+
if (value - hdrSize < 0) {
1614+
if (LOG.isTraceEnabled()) {
1615+
LOG.trace("onDiskSizeWithHeader={}, hdrSize={}; don't accept a value that is negative"
1616+
+ " after the header size is excluded.", value, hdrSize);
1617+
}
1618+
return false;
16111619
}
1612-
return (int) onDiskSizeWithHeaderL;
1620+
return true;
16131621
}
16141622

16151623
/**
1616-
* Verify the passed in onDiskSizeWithHeader aligns with what is in the header else something is
1617-
* not right.
1624+
* Check that {@code value} provided by the calling context seems reasonable, within a large
1625+
* margin of error.
1626+
* @return {@code true} if the value is safe to proceed, {@code false} otherwise.
16181627
*/
1619-
private void verifyOnDiskSizeMatchesHeader(final int passedIn, final ByteBuff headerBuf,
1620-
final long offset, boolean verifyChecksum) throws IOException {
1621-
// Assert size provided aligns with what is in the header
1622-
int fromHeader = getOnDiskSizeWithHeader(headerBuf, verifyChecksum);
1623-
if (passedIn != fromHeader) {
1624-
throw new IOException("Passed in onDiskSizeWithHeader=" + passedIn + " != " + fromHeader
1625-
+ ", offset=" + offset + ", fileContext=" + this.fileContext);
1628+
private boolean checkCallerProvidedOnDiskSizeWithHeader(long value) {
1629+
// same validation logic as is used by Math.toIntExact(long)
1630+
int intValue = (int) value;
1631+
if (intValue != value) {
1632+
if (LOG.isTraceEnabled()) {
1633+
LOG.trace("onDiskSizeWithHeaderL={}; value exceeds int size limits.", value);
1634+
}
1635+
return false;
1636+
}
1637+
if (intValue == -1) {
1638+
// a magic value we expect to see.
1639+
return true;
16261640
}
1641+
return checkOnDiskSizeWithHeader(intValue);
16271642
}
16281643

16291644
/**
@@ -1654,14 +1669,16 @@ private void cacheNextBlockHeader(final long offset, ByteBuff onDiskBlock,
16541669
this.prefetchedHeader.set(ph);
16551670
}
16561671

1657-
private int getNextBlockOnDiskSize(boolean readNextHeader, ByteBuff onDiskBlock,
1658-
int onDiskSizeWithHeader) {
1659-
int nextBlockOnDiskSize = -1;
1660-
if (readNextHeader) {
1661-
nextBlockOnDiskSize =
1662-
onDiskBlock.getIntAfterPosition(onDiskSizeWithHeader + BlockType.MAGIC_LENGTH) + hdrSize;
1663-
}
1664-
return nextBlockOnDiskSize;
1672+
/**
1673+
* Clear the cached value when its integrity is suspect.
1674+
*/
1675+
private void invalidateNextBlockHeader() {
1676+
prefetchedHeader.set(null);
1677+
}
1678+
1679+
private int getNextBlockOnDiskSize(ByteBuff onDiskBlock, int onDiskSizeWithHeader) {
1680+
return onDiskBlock.getIntAfterPosition(onDiskSizeWithHeader + BlockType.MAGIC_LENGTH)
1681+
+ hdrSize;
16651682
}
16661683

16671684
private ByteBuff allocate(int size, boolean intoHeap) {
@@ -1687,17 +1704,21 @@ private ByteBuff allocate(int size, boolean intoHeap) {
16871704
protected HFileBlock readBlockDataInternal(FSDataInputStream is, long offset,
16881705
long onDiskSizeWithHeaderL, boolean pread, boolean verifyChecksum, boolean updateMetrics,
16891706
boolean intoHeap) throws IOException {
1707+
final Span span = Span.current();
1708+
final AttributesBuilder attributesBuilder = Attributes.builder();
1709+
Optional.of(Context.current()).map(val -> val.get(CONTEXT_KEY))
1710+
.ifPresent(c -> c.accept(attributesBuilder));
16901711
if (offset < 0) {
16911712
throw new IOException("Invalid offset=" + offset + " trying to read " + "block (onDiskSize="
16921713
+ onDiskSizeWithHeaderL + ")");
16931714
}
1715+
if (!checkCallerProvidedOnDiskSizeWithHeader(onDiskSizeWithHeaderL)) {
1716+
LOG.trace("Caller provided invalid onDiskSizeWithHeaderL={}", onDiskSizeWithHeaderL);
1717+
onDiskSizeWithHeaderL = -1;
1718+
}
1719+
int onDiskSizeWithHeader = (int) onDiskSizeWithHeaderL;
16941720

1695-
final Span span = Span.current();
1696-
final AttributesBuilder attributesBuilder = Attributes.builder();
1697-
Optional.of(Context.current()).map(val -> val.get(CONTEXT_KEY))
1698-
.ifPresent(c -> c.accept(attributesBuilder));
1699-
int onDiskSizeWithHeader = checkAndGetSizeAsInt(onDiskSizeWithHeaderL, hdrSize);
1700-
// Try and get cached header. Will serve us in rare case where onDiskSizeWithHeaderL is -1
1721+
// Try to use the cached header. Will serve us in rare case where onDiskSizeWithHeaderL==-1
17011722
// and will save us having to seek the stream backwards to reread the header we
17021723
// read the last time through here.
17031724
ByteBuff headerBuf = getCachedHeader(offset);
@@ -1711,8 +1732,8 @@ protected HFileBlock readBlockDataInternal(FSDataInputStream is, long offset,
17111732
// file has support for checksums (version 2+).
17121733
boolean checksumSupport = this.fileContext.isUseHBaseChecksum();
17131734
long startTime = EnvironmentEdgeManager.currentTime();
1714-
if (onDiskSizeWithHeader <= 0) {
1715-
// We were not passed the block size. Need to get it from the header. If header was
1735+
if (onDiskSizeWithHeader == -1) {
1736+
// The caller does not know the block size. Need to get it from the header. If header was
17161737
// not cached (see getCachedHeader above), need to seek to pull it in. This is costly
17171738
// and should happen very rarely. Currently happens on open of a hfile reader where we
17181739
// read the trailer blocks to pull in the indices. Otherwise, we are reading block sizes
@@ -1729,6 +1750,19 @@ protected HFileBlock readBlockDataInternal(FSDataInputStream is, long offset,
17291750
}
17301751
onDiskSizeWithHeader = getOnDiskSizeWithHeader(headerBuf, checksumSupport);
17311752
}
1753+
1754+
// The common case is that onDiskSizeWithHeader was produced by a read without checksum
1755+
// validation, so give it a sanity check before trying to use it.
1756+
if (!checkOnDiskSizeWithHeader(onDiskSizeWithHeader)) {
1757+
if (verifyChecksum) {
1758+
invalidateNextBlockHeader();
1759+
span.addEvent("Falling back to HDFS checksumming.", attributesBuilder.build());
1760+
return null;
1761+
} else {
1762+
throw new IOException("Invalid onDiskSizeWithHeader=" + onDiskSizeWithHeader);
1763+
}
1764+
}
1765+
17321766
int preReadHeaderSize = headerBuf == null ? 0 : hdrSize;
17331767
// Allocate enough space to fit the next block's header too; saves a seek next time through.
17341768
// onDiskBlock is whole block + header + checksums then extra hdrSize to read next header;
@@ -1745,19 +1779,49 @@ protected HFileBlock readBlockDataInternal(FSDataInputStream is, long offset,
17451779
boolean readNextHeader = readAtOffset(is, onDiskBlock,
17461780
onDiskSizeWithHeader - preReadHeaderSize, true, offset + preReadHeaderSize, pread);
17471781
onDiskBlock.rewind(); // in case of moving position when copying a cached header
1748-
int nextBlockOnDiskSize =
1749-
getNextBlockOnDiskSize(readNextHeader, onDiskBlock, onDiskSizeWithHeader);
1782+
1783+
// the call to validateChecksum for this block excludes the next block header over-read, so
1784+
// no reason to delay extracting this value.
1785+
int nextBlockOnDiskSize = -1;
1786+
if (readNextHeader) {
1787+
int parsedVal = getNextBlockOnDiskSize(onDiskBlock, onDiskSizeWithHeader);
1788+
if (checkOnDiskSizeWithHeader(parsedVal)) {
1789+
nextBlockOnDiskSize = parsedVal;
1790+
}
1791+
}
17501792
if (headerBuf == null) {
17511793
headerBuf = onDiskBlock.duplicate().position(0).limit(hdrSize);
17521794
}
1753-
// Do a few checks before we go instantiate HFileBlock.
1754-
assert onDiskSizeWithHeader > this.hdrSize;
1755-
verifyOnDiskSizeMatchesHeader(onDiskSizeWithHeader, headerBuf, offset, checksumSupport);
1795+
17561796
ByteBuff curBlock = onDiskBlock.duplicate().position(0).limit(onDiskSizeWithHeader);
17571797
// Verify checksum of the data before using it for building HFileBlock.
17581798
if (verifyChecksum && !validateChecksum(offset, curBlock, hdrSize)) {
1799+
invalidateNextBlockHeader();
1800+
span.addEvent("Falling back to HDFS checksumming.", attributesBuilder.build());
17591801
return null;
17601802
}
1803+
1804+
// TODO: is this check necessary or can we proceed with a provided value regardless of
1805+
// what is in the header?
1806+
int fromHeader = getOnDiskSizeWithHeader(headerBuf, checksumSupport);
1807+
if (onDiskSizeWithHeader != fromHeader) {
1808+
if (LOG.isTraceEnabled()) {
1809+
LOG.trace("Passed in onDiskSizeWithHeader={} != {}, offset={}, fileContext={}",
1810+
onDiskSizeWithHeader, fromHeader, offset, this.fileContext);
1811+
}
1812+
if (checksumSupport && verifyChecksum) {
1813+
// This file supports HBase checksums and verification of those checksums was
1814+
// requested. The block size provided by the caller (presumably from the block index)
1815+
// does not match the block size written to the block header. treat this as
1816+
// HBase-checksum failure.
1817+
span.addEvent("Falling back to HDFS checksumming.", attributesBuilder.build());
1818+
invalidateNextBlockHeader();
1819+
return null;
1820+
}
1821+
throw new IOException("Passed in onDiskSizeWithHeader=" + onDiskSizeWithHeader + " != "
1822+
+ fromHeader + ", offset=" + offset + ", fileContext=" + this.fileContext);
1823+
}
1824+
17611825
// remove checksum from buffer now that it's verified
17621826
int sizeWithoutChecksum = curBlock.getInt(Header.ON_DISK_DATA_SIZE_WITH_HEADER_INDEX);
17631827
curBlock.limit(sizeWithoutChecksum);

hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestChecksum.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public class TestChecksum {
6161
public static final HBaseClassTestRule CLASS_RULE =
6262
HBaseClassTestRule.forClass(TestChecksum.class);
6363

64-
private static final Logger LOG = LoggerFactory.getLogger(TestHFileBlock.class);
64+
private static final Logger LOG = LoggerFactory.getLogger(TestChecksum.class);
6565

6666
static final Compression.Algorithm[] COMPRESSION_ALGORITHMS = { NONE, GZ };
6767

hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestHFile.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,7 @@ public void testReaderWithoutBlockCache() throws Exception {
163163
fillByteBuffAllocator(alloc, bufCount);
164164
// start write to store file.
165165
Path path = writeStoreFile();
166-
try {
167-
readStoreFile(path, conf, alloc);
168-
} catch (Exception e) {
169-
// fail test
170-
assertTrue(false);
171-
}
166+
readStoreFile(path, conf, alloc);
172167
Assert.assertEquals(bufCount, alloc.getFreeBufferCount());
173168
alloc.clean();
174169
}

0 commit comments

Comments
 (0)