Skip to content

Commit df2328a

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 <bbeaudreault@apache.org>
1 parent e3457d4 commit df2328a

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
@@ -390,12 +390,12 @@ static HFileBlock createFromBuff(ByteBuff buf, boolean usesHBaseChecksum, final
390390

391391
/**
392392
* Parse total on disk size including header and checksum.
393-
* @param headerBuf Header ByteBuffer. Presumed exact size of header.
394-
* @param verifyChecksum true if checksum verification is in use.
393+
* @param headerBuf Header ByteBuffer. Presumed exact size of header.
394+
* @param checksumSupport true if checksum verification is in use.
395395
* @return Size of the block with header included.
396396
*/
397-
private static int getOnDiskSizeWithHeader(final ByteBuff headerBuf, boolean verifyChecksum) {
398-
return headerBuf.getInt(Header.ON_DISK_SIZE_WITHOUT_HEADER_INDEX) + headerSize(verifyChecksum);
397+
private static int getOnDiskSizeWithHeader(final ByteBuff headerBuf, boolean checksumSupport) {
398+
return headerBuf.getInt(Header.ON_DISK_SIZE_WITHOUT_HEADER_INDEX) + headerSize(checksumSupport);
399399
}
400400

401401
/**
@@ -1568,33 +1568,48 @@ public HFileBlock readBlockData(long offset, long onDiskSizeWithHeaderL, boolean
15681568
}
15691569

15701570
/**
1571-
* Returns Check <code>onDiskSizeWithHeaderL</code> size is healthy and then return it as an int
1571+
* Check that {@code value} read from a block header seems reasonable, within a large margin of
1572+
* error.
1573+
* @return {@code true} if the value is safe to proceed, {@code false} otherwise.
15721574
*/
1573-
private static int checkAndGetSizeAsInt(final long onDiskSizeWithHeaderL, final int hdrSize)
1574-
throws IOException {
1575-
if (
1576-
(onDiskSizeWithHeaderL < hdrSize && onDiskSizeWithHeaderL != -1)
1577-
|| onDiskSizeWithHeaderL >= Integer.MAX_VALUE
1578-
) {
1579-
throw new IOException(
1580-
"Invalid onDisksize=" + onDiskSizeWithHeaderL + ": expected to be at least " + hdrSize
1581-
+ " and at most " + Integer.MAX_VALUE + ", or -1");
1575+
private boolean checkOnDiskSizeWithHeader(int value) {
1576+
if (value < 0) {
1577+
if (LOG.isTraceEnabled()) {
1578+
LOG.trace(
1579+
"onDiskSizeWithHeader={}; value represents a size, so it should never be negative.",
1580+
value);
1581+
}
1582+
return false;
1583+
}
1584+
if (value - hdrSize < 0) {
1585+
if (LOG.isTraceEnabled()) {
1586+
LOG.trace("onDiskSizeWithHeader={}, hdrSize={}; don't accept a value that is negative"
1587+
+ " after the header size is excluded.", value, hdrSize);
1588+
}
1589+
return false;
15821590
}
1583-
return (int) onDiskSizeWithHeaderL;
1591+
return true;
15841592
}
15851593

15861594
/**
1587-
* Verify the passed in onDiskSizeWithHeader aligns with what is in the header else something is
1588-
* not right.
1595+
* Check that {@code value} provided by the calling context seems reasonable, within a large
1596+
* margin of error.
1597+
* @return {@code true} if the value is safe to proceed, {@code false} otherwise.
15891598
*/
1590-
private void verifyOnDiskSizeMatchesHeader(final int passedIn, final ByteBuff headerBuf,
1591-
final long offset, boolean verifyChecksum) throws IOException {
1592-
// Assert size provided aligns with what is in the header
1593-
int fromHeader = getOnDiskSizeWithHeader(headerBuf, verifyChecksum);
1594-
if (passedIn != fromHeader) {
1595-
throw new IOException("Passed in onDiskSizeWithHeader=" + passedIn + " != " + fromHeader
1596-
+ ", offset=" + offset + ", fileContext=" + this.fileContext);
1599+
private boolean checkCallerProvidedOnDiskSizeWithHeader(long value) {
1600+
// same validation logic as is used by Math.toIntExact(long)
1601+
int intValue = (int) value;
1602+
if (intValue != value) {
1603+
if (LOG.isTraceEnabled()) {
1604+
LOG.trace("onDiskSizeWithHeaderL={}; value exceeds int size limits.", value);
1605+
}
1606+
return false;
15971607
}
1608+
if (intValue == -1) {
1609+
// a magic value we expect to see.
1610+
return true;
1611+
}
1612+
return checkOnDiskSizeWithHeader(intValue);
15981613
}
15991614

16001615
/**
@@ -1625,14 +1640,16 @@ private void cacheNextBlockHeader(final long offset, ByteBuff onDiskBlock,
16251640
this.prefetchedHeader.set(ph);
16261641
}
16271642

1628-
private int getNextBlockOnDiskSize(boolean readNextHeader, ByteBuff onDiskBlock,
1629-
int onDiskSizeWithHeader) {
1630-
int nextBlockOnDiskSize = -1;
1631-
if (readNextHeader) {
1632-
nextBlockOnDiskSize =
1633-
onDiskBlock.getIntAfterPosition(onDiskSizeWithHeader + BlockType.MAGIC_LENGTH) + hdrSize;
1634-
}
1635-
return nextBlockOnDiskSize;
1643+
/**
1644+
* Clear the cached value when its integrity is suspect.
1645+
*/
1646+
private void invalidateNextBlockHeader() {
1647+
prefetchedHeader.set(null);
1648+
}
1649+
1650+
private int getNextBlockOnDiskSize(ByteBuff onDiskBlock, int onDiskSizeWithHeader) {
1651+
return onDiskBlock.getIntAfterPosition(onDiskSizeWithHeader + BlockType.MAGIC_LENGTH)
1652+
+ hdrSize;
16361653
}
16371654

16381655
private ByteBuff allocate(int size, boolean intoHeap) {
@@ -1658,17 +1675,21 @@ private ByteBuff allocate(int size, boolean intoHeap) {
16581675
protected HFileBlock readBlockDataInternal(FSDataInputStream is, long offset,
16591676
long onDiskSizeWithHeaderL, boolean pread, boolean verifyChecksum, boolean updateMetrics,
16601677
boolean intoHeap) throws IOException {
1678+
final Span span = Span.current();
1679+
final AttributesBuilder attributesBuilder = Attributes.builder();
1680+
Optional.of(Context.current()).map(val -> val.get(CONTEXT_KEY))
1681+
.ifPresent(c -> c.accept(attributesBuilder));
16611682
if (offset < 0) {
16621683
throw new IOException("Invalid offset=" + offset + " trying to read " + "block (onDiskSize="
16631684
+ onDiskSizeWithHeaderL + ")");
16641685
}
1686+
if (!checkCallerProvidedOnDiskSizeWithHeader(onDiskSizeWithHeaderL)) {
1687+
LOG.trace("Caller provided invalid onDiskSizeWithHeaderL={}", onDiskSizeWithHeaderL);
1688+
onDiskSizeWithHeaderL = -1;
1689+
}
1690+
int onDiskSizeWithHeader = (int) onDiskSizeWithHeaderL;
16651691

1666-
final Span span = Span.current();
1667-
final AttributesBuilder attributesBuilder = Attributes.builder();
1668-
Optional.of(Context.current()).map(val -> val.get(CONTEXT_KEY))
1669-
.ifPresent(c -> c.accept(attributesBuilder));
1670-
int onDiskSizeWithHeader = checkAndGetSizeAsInt(onDiskSizeWithHeaderL, hdrSize);
1671-
// Try and get cached header. Will serve us in rare case where onDiskSizeWithHeaderL is -1
1692+
// Try to use the cached header. Will serve us in rare case where onDiskSizeWithHeaderL==-1
16721693
// and will save us having to seek the stream backwards to reread the header we
16731694
// read the last time through here.
16741695
ByteBuff headerBuf = getCachedHeader(offset);
@@ -1682,8 +1703,8 @@ protected HFileBlock readBlockDataInternal(FSDataInputStream is, long offset,
16821703
// file has support for checksums (version 2+).
16831704
boolean checksumSupport = this.fileContext.isUseHBaseChecksum();
16841705
long startTime = EnvironmentEdgeManager.currentTime();
1685-
if (onDiskSizeWithHeader <= 0) {
1686-
// We were not passed the block size. Need to get it from the header. If header was
1706+
if (onDiskSizeWithHeader == -1) {
1707+
// The caller does not know the block size. Need to get it from the header. If header was
16871708
// not cached (see getCachedHeader above), need to seek to pull it in. This is costly
16881709
// and should happen very rarely. Currently happens on open of a hfile reader where we
16891710
// read the trailer blocks to pull in the indices. Otherwise, we are reading block sizes
@@ -1700,6 +1721,19 @@ protected HFileBlock readBlockDataInternal(FSDataInputStream is, long offset,
17001721
}
17011722
onDiskSizeWithHeader = getOnDiskSizeWithHeader(headerBuf, checksumSupport);
17021723
}
1724+
1725+
// The common case is that onDiskSizeWithHeader was produced by a read without checksum
1726+
// validation, so give it a sanity check before trying to use it.
1727+
if (!checkOnDiskSizeWithHeader(onDiskSizeWithHeader)) {
1728+
if (verifyChecksum) {
1729+
invalidateNextBlockHeader();
1730+
span.addEvent("Falling back to HDFS checksumming.", attributesBuilder.build());
1731+
return null;
1732+
} else {
1733+
throw new IOException("Invalid onDiskSizeWithHeader=" + onDiskSizeWithHeader);
1734+
}
1735+
}
1736+
17031737
int preReadHeaderSize = headerBuf == null ? 0 : hdrSize;
17041738
// Allocate enough space to fit the next block's header too; saves a seek next time through.
17051739
// onDiskBlock is whole block + header + checksums then extra hdrSize to read next header;
@@ -1716,19 +1750,49 @@ protected HFileBlock readBlockDataInternal(FSDataInputStream is, long offset,
17161750
boolean readNextHeader = readAtOffset(is, onDiskBlock,
17171751
onDiskSizeWithHeader - preReadHeaderSize, true, offset + preReadHeaderSize, pread);
17181752
onDiskBlock.rewind(); // in case of moving position when copying a cached header
1719-
int nextBlockOnDiskSize =
1720-
getNextBlockOnDiskSize(readNextHeader, onDiskBlock, onDiskSizeWithHeader);
1753+
1754+
// the call to validateChecksum for this block excludes the next block header over-read, so
1755+
// no reason to delay extracting this value.
1756+
int nextBlockOnDiskSize = -1;
1757+
if (readNextHeader) {
1758+
int parsedVal = getNextBlockOnDiskSize(onDiskBlock, onDiskSizeWithHeader);
1759+
if (checkOnDiskSizeWithHeader(parsedVal)) {
1760+
nextBlockOnDiskSize = parsedVal;
1761+
}
1762+
}
17211763
if (headerBuf == null) {
17221764
headerBuf = onDiskBlock.duplicate().position(0).limit(hdrSize);
17231765
}
1724-
// Do a few checks before we go instantiate HFileBlock.
1725-
assert onDiskSizeWithHeader > this.hdrSize;
1726-
verifyOnDiskSizeMatchesHeader(onDiskSizeWithHeader, headerBuf, offset, checksumSupport);
1766+
17271767
ByteBuff curBlock = onDiskBlock.duplicate().position(0).limit(onDiskSizeWithHeader);
17281768
// Verify checksum of the data before using it for building HFileBlock.
17291769
if (verifyChecksum && !validateChecksum(offset, curBlock, hdrSize)) {
1770+
invalidateNextBlockHeader();
1771+
span.addEvent("Falling back to HDFS checksumming.", attributesBuilder.build());
17301772
return null;
17311773
}
1774+
1775+
// TODO: is this check necessary or can we proceed with a provided value regardless of
1776+
// what is in the header?
1777+
int fromHeader = getOnDiskSizeWithHeader(headerBuf, checksumSupport);
1778+
if (onDiskSizeWithHeader != fromHeader) {
1779+
if (LOG.isTraceEnabled()) {
1780+
LOG.trace("Passed in onDiskSizeWithHeader={} != {}, offset={}, fileContext={}",
1781+
onDiskSizeWithHeader, fromHeader, offset, this.fileContext);
1782+
}
1783+
if (checksumSupport && verifyChecksum) {
1784+
// This file supports HBase checksums and verification of those checksums was
1785+
// requested. The block size provided by the caller (presumably from the block index)
1786+
// does not match the block size written to the block header. treat this as
1787+
// HBase-checksum failure.
1788+
span.addEvent("Falling back to HDFS checksumming.", attributesBuilder.build());
1789+
invalidateNextBlockHeader();
1790+
return null;
1791+
}
1792+
throw new IOException("Passed in onDiskSizeWithHeader=" + onDiskSizeWithHeader + " != "
1793+
+ fromHeader + ", offset=" + offset + ", fileContext=" + this.fileContext);
1794+
}
1795+
17321796
// remove checksum from buffer now that it's verified
17331797
int sizeWithoutChecksum = curBlock.getInt(Header.ON_DISK_DATA_SIZE_WITH_HEADER_INDEX);
17341798
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)