Add chunk-level checksum to RecordedTransaction
Test: `atest binderUnitTest`
Change-Id: Id080925da4eb31821c01d7b3178844fc727b6c3e
diff --git a/libs/binder/BinderRecordReplay.cpp b/libs/binder/BinderRecordReplay.cpp
index 8ba18a8..58bb106 100644
--- a/libs/binder/BinderRecordReplay.cpp
+++ b/libs/binder/BinderRecordReplay.cpp
@@ -18,6 +18,7 @@
#include <android-base/logging.h>
#include <android-base/unique_fd.h>
#include <binder/BinderRecordReplay.h>
+#include <sys/mman.h>
#include <algorithm>
using android::Parcel;
@@ -42,34 +43,38 @@
//
// A RecordedTransaction is written to a file as a sequence of Chunks.
//
-// A Chunk consists of a ChunkDescriptor, Data, and Padding.
+// A Chunk consists of a ChunkDescriptor, Data, Padding, and a Checksum.
//
-// Data and Padding may each be zero-length as specified by the
-// ChunkDescriptor.
+// The ChunkDescriptor identifies the type of Data in the chunk, and the size
+// of the Data.
//
-// The ChunkDescriptor identifies the type of data in the chunk, the size of
-// the data in bytes, and the number of zero-bytes padding to land on an
-// 8-byte boundary by the end of the Chunk.
+// The Data may be any uint32 number of bytes in length in [0-0xfffffff0].
+//
+// Padding is between [0-7] zero-bytes after the Data such that the Chunk ends
+// on an 8-byte boundary. The ChunkDescriptor's dataSize does not include the
+// size of Padding.
+//
+// The checksum is a 64-bit wide XOR of all previous data from the start of the
+// ChunkDescriptor to the end of Padding.
//
// ┌───────────────────────────┐
// │Chunk │
+// │┌────────────────────────┐ │
+// ││ChunkDescriptor │ │
+// ││┌───────────┬──────────┐│ │
+// │││chunkType │dataSize ├┼─┼─┐
+// │││uint32_t │uint32_t ││ │ │
+// ││└───────────┴──────────┘│ │ │
+// │└────────────────────────┘ │ │
+// │┌─────────────────────────┐│ │
+// ││Data ││ │
+// ││bytes * dataSize │◀─┘
+// ││ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤│
+// ││ Padding ││
+// │└───┴─────────────────────┘│
// │┌─────────────────────────┐│
-// ││ChunkDescriptor ││
-// ││┌───────────┬───────────┐││
-// │││chunkType │paddingSize│││
-// │││uint32_t │uint32_t ├┼┼───┐
-// ││├───────────┴───────────┤││ │
-// │││dataSize │││ │
-// │││uint64_t ├┼┼─┐ │
-// ││└───────────────────────┘││ │ │
-// │└─────────────────────────┘│ │ │
-// │┌─────────────────────────┐│ │ │
-// ││Data ││ │ │
-// ││bytes * dataSize │◀─┘ │
-// │└─────────────────────────┘│ │
-// │┌─────────────────────────┐│ │
-// ││Padding ││ │
-// ││bytes * paddingSize │◀───┘
+// ││checksum ││
+// ││uint64_t ││
// │└─────────────────────────┘│
// └───────────────────────────┘
//
@@ -85,20 +90,20 @@
// ║ End Chunk ║
// ╚══════════════════════╝
//
-// On reading a RecordedTransaction, an unrecognized chunk is skipped using
-// the size information in the ChunkDescriptor. Chunks are read and either
-// assimilated or skipped until an End Chunk is encountered. This has three
-// notable implications:
+// On reading a RecordedTransaction, an unrecognized chunk is checksummed
+// then skipped according to size information in the ChunkDescriptor. Chunks
+// are read and either assimilated or skipped until an End Chunk is
+// encountered. This has three notable implications:
//
// 1. Older and newer implementations should be able to read one another's
// Transactions, though there will be loss of information.
-// 2. With the exception of the End Chunk, Chunks can appear in any
-// order and even repeat, though this is not recommended.
+// 2. With the exception of the End Chunk, Chunks can appear in any order
+// and even repeat, though this is not recommended.
// 3. If any Chunk is repeated, old values will be overwritten by versions
// encountered later in the file.
//
// No effort is made to ensure the expected chunks are present. A single
-// End Chunk may therefore produce a empty, meaningless RecordedTransaction.
+// End Chunk may therefore produce an empty, meaningless RecordedTransaction.
RecordedTransaction::RecordedTransaction(RecordedTransaction&& t) noexcept {
mHeader = t.mHeader;
@@ -121,12 +126,12 @@
0};
if (t.mSent.setData(dataParcel.data(), dataParcel.dataSize()) != android::NO_ERROR) {
- LOG(INFO) << "Failed to set sent parcel data.";
+ LOG(ERROR) << "Failed to set sent parcel data.";
return std::nullopt;
}
if (t.mReply.setData(replyParcel.data(), replyParcel.dataSize()) != android::NO_ERROR) {
- LOG(INFO) << "Failed to set reply parcel data.";
+ LOG(ERROR) << "Failed to set reply parcel data.";
return std::nullopt;
}
@@ -134,94 +139,111 @@
}
enum {
- HEADER_CHUNK = 0x00000001,
- DATA_PARCEL_CHUNK = 0x00000002,
- REPLY_PARCEL_CHUNK = 0x00000003,
- INVALID_CHUNK = 0x00fffffe,
+ HEADER_CHUNK = 1,
+ DATA_PARCEL_CHUNK = 2,
+ REPLY_PARCEL_CHUNK = 3,
END_CHUNK = 0x00ffffff,
};
struct ChunkDescriptor {
uint32_t chunkType = 0;
- uint32_t padding = 0;
uint32_t dataSize = 0;
- uint32_t reserved = 0; // Future checksum
};
+static_assert(sizeof(ChunkDescriptor) % 8 == 0);
-static android::status_t readChunkDescriptor(borrowed_fd fd, ChunkDescriptor* chunkOut) {
+constexpr uint32_t kMaxChunkDataSize = 0xfffffff0;
+typedef uint64_t transaction_checksum_t;
+
+static android::status_t readChunkDescriptor(borrowed_fd fd, ChunkDescriptor* chunkOut,
+ transaction_checksum_t* sum) {
if (!android::base::ReadFully(fd, chunkOut, sizeof(ChunkDescriptor))) {
- LOG(INFO) << "Failed to read Chunk Descriptor from fd " << fd.get();
+ LOG(ERROR) << "Failed to read Chunk Descriptor from fd " << fd.get();
return android::UNKNOWN_ERROR;
}
- if (PADDING8(chunkOut->dataSize) != chunkOut->padding) {
- chunkOut->chunkType = INVALID_CHUNK;
- LOG(INFO) << "Chunk data and padding sizes do not align." << fd.get();
- return android::BAD_VALUE;
- }
+
+ *sum ^= *reinterpret_cast<transaction_checksum_t*>(chunkOut);
return android::NO_ERROR;
}
std::optional<RecordedTransaction> RecordedTransaction::fromFile(const unique_fd& fd) {
RecordedTransaction t;
ChunkDescriptor chunk;
-
+ const long pageSize = sysconf(_SC_PAGE_SIZE);
do {
- if (NO_ERROR != readChunkDescriptor(fd, &chunk)) {
- LOG(INFO) << "Failed to read chunk descriptor.";
+ transaction_checksum_t checksum = 0;
+ if (NO_ERROR != readChunkDescriptor(fd, &chunk, &checksum)) {
+ LOG(ERROR) << "Failed to read chunk descriptor.";
return std::nullopt;
}
+ off_t fdCurrentPosition = lseek(fd.get(), 0, SEEK_CUR);
+ off_t mmapPageAlignedStart = (fdCurrentPosition / pageSize) * pageSize;
+ off_t mmapPayloadStartOffset = fdCurrentPosition - mmapPageAlignedStart;
+
+ if (chunk.dataSize > kMaxChunkDataSize) {
+ LOG(ERROR) << "Chunk data exceeds maximum size.";
+ return std::nullopt;
+ }
+
+ size_t chunkPayloadSize =
+ chunk.dataSize + PADDING8(chunk.dataSize) + sizeof(transaction_checksum_t);
+
+ if (PADDING8(chunkPayloadSize) != 0) {
+ LOG(ERROR) << "Invalid chunk size, not aligned " << chunkPayloadSize;
+ return std::nullopt;
+ }
+
+ transaction_checksum_t* payloadMap = reinterpret_cast<transaction_checksum_t*>(
+ mmap(NULL, chunkPayloadSize + mmapPayloadStartOffset, PROT_READ, MAP_SHARED,
+ fd.get(), mmapPageAlignedStart));
+ payloadMap += mmapPayloadStartOffset /
+ sizeof(transaction_checksum_t); // Skip chunk descriptor and required mmap
+ // page-alignment
+ if (payloadMap == MAP_FAILED) {
+ LOG(ERROR) << "Memory mapping failed for fd " << fd.get() << ": " << errno << " "
+ << strerror(errno);
+ return std::nullopt;
+ }
+ for (size_t checksumIndex = 0;
+ checksumIndex < chunkPayloadSize / sizeof(transaction_checksum_t); checksumIndex++) {
+ checksum ^= payloadMap[checksumIndex];
+ }
+ if (checksum != 0) {
+ LOG(ERROR) << "Checksum failed.";
+ return std::nullopt;
+ }
+ lseek(fd.get(), chunkPayloadSize, SEEK_CUR);
+
switch (chunk.chunkType) {
case HEADER_CHUNK: {
if (chunk.dataSize != static_cast<uint32_t>(sizeof(TransactionHeader))) {
- LOG(INFO) << "Header Chunk indicated size " << chunk.dataSize << "; Expected "
- << sizeof(TransactionHeader) << ".";
+ LOG(ERROR) << "Header Chunk indicated size " << chunk.dataSize << "; Expected "
+ << sizeof(TransactionHeader) << ".";
return std::nullopt;
}
- if (!android::base::ReadFully(fd, &t.mHeader, chunk.dataSize)) {
- LOG(INFO) << "Failed to read transactionHeader from fd " << fd.get();
- return std::nullopt;
- }
- lseek(fd.get(), chunk.padding, SEEK_CUR);
+ t.mHeader = *reinterpret_cast<TransactionHeader*>(payloadMap);
break;
}
case DATA_PARCEL_CHUNK: {
- std::vector<uint8_t> bytes;
- bytes.resize(chunk.dataSize);
- if (!android::base::ReadFully(fd, bytes.data(), chunk.dataSize)) {
- LOG(INFO) << "Failed to read sent parcel data from fd " << fd.get();
+ if (t.mSent.setData(reinterpret_cast<const unsigned char*>(payloadMap),
+ chunk.dataSize) != android::NO_ERROR) {
+ LOG(ERROR) << "Failed to set sent parcel data.";
return std::nullopt;
}
- if (t.mSent.setData(bytes.data(), chunk.dataSize) != android::NO_ERROR) {
- LOG(INFO) << "Failed to set sent parcel data.";
- return std::nullopt;
- }
- lseek(fd.get(), chunk.padding, SEEK_CUR);
break;
}
case REPLY_PARCEL_CHUNK: {
- std::vector<uint8_t> bytes;
- bytes.resize(chunk.dataSize);
- if (!android::base::ReadFully(fd, bytes.data(), chunk.dataSize)) {
- LOG(INFO) << "Failed to read reply parcel data from fd " << fd.get();
+ if (t.mReply.setData(reinterpret_cast<const unsigned char*>(payloadMap),
+ chunk.dataSize) != android::NO_ERROR) {
+ LOG(ERROR) << "Failed to set reply parcel data.";
return std::nullopt;
}
- if (t.mReply.setData(bytes.data(), chunk.dataSize) != android::NO_ERROR) {
- LOG(INFO) << "Failed to set reply parcel data.";
- return std::nullopt;
- }
- lseek(fd.get(), chunk.padding, SEEK_CUR);
break;
}
- case INVALID_CHUNK:
- LOG(INFO) << "Invalid chunk.";
- return std::nullopt;
case END_CHUNK:
- LOG(INFO) << "Read end chunk";
- FALLTHROUGH_INTENDED;
- default:
- // Unrecognized or skippable chunk
- lseek(fd.get(), chunk.dataSize + chunk.padding, SEEK_CUR);
break;
+ default:
+ LOG(INFO) << "Unrecognized chunk.";
+ continue;
}
} while (chunk.chunkType != END_CHUNK);
@@ -230,36 +252,37 @@
android::status_t RecordedTransaction::writeChunk(borrowed_fd fd, uint32_t chunkType,
size_t byteCount, const uint8_t* data) const {
- // Write Chunk Descriptor
- // - Chunk Type
- if (!android::base::WriteFully(fd, &chunkType, sizeof(uint32_t))) {
- LOG(INFO) << "Failed to write chunk header to fd " << fd.get();
- return UNKNOWN_ERROR;
+ if (byteCount > kMaxChunkDataSize) {
+ LOG(ERROR) << "Chunk data exceeds maximum size";
+ return BAD_VALUE;
}
- // - Chunk Data Padding Size
- uint32_t additionalPaddingCount = static_cast<uint32_t>(PADDING8(byteCount));
- if (!android::base::WriteFully(fd, &additionalPaddingCount, sizeof(uint32_t))) {
- LOG(INFO) << "Failed to write chunk padding size to fd " << fd.get();
- return UNKNOWN_ERROR;
- }
- // - Chunk Data Size
- uint64_t byteCountToWrite = (uint64_t)byteCount;
- if (!android::base::WriteFully(fd, &byteCountToWrite, sizeof(uint64_t))) {
- LOG(INFO) << "Failed to write chunk size to fd " << fd.get();
- return UNKNOWN_ERROR;
- }
- if (byteCount == 0) {
- return NO_ERROR;
+ ChunkDescriptor descriptor = {.chunkType = chunkType,
+ .dataSize = static_cast<uint32_t>(byteCount)};
+ // Prepare Chunk content as byte *
+ const std::byte* descriptorBytes = reinterpret_cast<const std::byte*>(&descriptor);
+ const std::byte* dataBytes = reinterpret_cast<const std::byte*>(data);
+
+ // Add Chunk to intermediate buffer, except checksum
+ std::vector<std::byte> buffer;
+ buffer.insert(buffer.end(), descriptorBytes, descriptorBytes + sizeof(ChunkDescriptor));
+ buffer.insert(buffer.end(), dataBytes, dataBytes + byteCount);
+ std::byte zero{0};
+ buffer.insert(buffer.end(), PADDING8(byteCount), zero);
+
+ // Calculate checksum from buffer
+ transaction_checksum_t* checksumData = reinterpret_cast<transaction_checksum_t*>(buffer.data());
+ transaction_checksum_t checksumValue = 0;
+ for (size_t idx = 0; idx < (buffer.size() / sizeof(transaction_checksum_t)); idx++) {
+ checksumValue ^= checksumData[idx];
}
- if (!android::base::WriteFully(fd, data, byteCount)) {
- LOG(INFO) << "Failed to write chunk data to fd " << fd.get();
- return UNKNOWN_ERROR;
- }
+ // Write checksum to buffer
+ std::byte* checksumBytes = reinterpret_cast<std::byte*>(&checksumValue);
+ buffer.insert(buffer.end(), checksumBytes, checksumBytes + sizeof(transaction_checksum_t));
- const uint8_t zeros[7] = {0};
- if (!android::base::WriteFully(fd, zeros, additionalPaddingCount)) {
- LOG(INFO) << "Failed to write chunk padding to fd " << fd.get();
+ // Write buffer to file
+ if (!android::base::WriteFully(fd, buffer.data(), buffer.size())) {
+ LOG(ERROR) << "Failed to write chunk fd " << fd.get();
return UNKNOWN_ERROR;
}
return NO_ERROR;
@@ -269,19 +292,19 @@
if (NO_ERROR !=
writeChunk(fd, HEADER_CHUNK, sizeof(TransactionHeader),
reinterpret_cast<const uint8_t*>(&mHeader))) {
- LOG(INFO) << "Failed to write transactionHeader to fd " << fd.get();
+ LOG(ERROR) << "Failed to write transactionHeader to fd " << fd.get();
return UNKNOWN_ERROR;
}
if (NO_ERROR != writeChunk(fd, DATA_PARCEL_CHUNK, mSent.dataSize(), mSent.data())) {
- LOG(INFO) << "Failed to write sent Parcel to fd " << fd.get();
+ LOG(ERROR) << "Failed to write sent Parcel to fd " << fd.get();
return UNKNOWN_ERROR;
}
if (NO_ERROR != writeChunk(fd, REPLY_PARCEL_CHUNK, mReply.dataSize(), mReply.data())) {
- LOG(INFO) << "Failed to write reply Parcel to fd " << fd.get();
+ LOG(ERROR) << "Failed to write reply Parcel to fd " << fd.get();
return UNKNOWN_ERROR;
}
if (NO_ERROR != writeChunk(fd, END_CHUNK, 0, NULL)) {
- LOG(INFO) << "Failed to write end chunk to fd " << fd.get();
+ LOG(ERROR) << "Failed to write end chunk to fd " << fd.get();
return UNKNOWN_ERROR;
}
return NO_ERROR;
diff --git a/libs/binder/tests/binderRecordedTransactionTest.cpp b/libs/binder/tests/binderRecordedTransactionTest.cpp
index 2ece315..67553fc 100644
--- a/libs/binder/tests/binderRecordedTransactionTest.cpp
+++ b/libs/binder/tests/binderRecordedTransactionTest.cpp
@@ -16,6 +16,7 @@
#include <binder/BinderRecordReplay.h>
#include <gtest/gtest.h>
+#include <utils/Errors.h>
using android::Parcel;
using android::status_t;
@@ -54,3 +55,80 @@
EXPECT_EQ(retrievedTransaction->getDataParcel().readInt64(), 2);
EXPECT_EQ(retrievedTransaction->getReplyParcel().readInt32(), 99);
}
+
+TEST(BinderRecordedTransaction, Checksum) {
+ Parcel d;
+ d.writeInt32(12);
+ d.writeInt64(2);
+ Parcel r;
+ r.writeInt32(99);
+ timespec ts = {1232456, 567890};
+ auto transaction = RecordedTransaction::fromDetails(1, 42, ts, d, r, 0);
+
+ auto file = std::tmpfile();
+ auto fd = unique_fd(fcntl(fileno(file), F_DUPFD, 1));
+
+ status_t status = transaction->dumpToFile(fd);
+ ASSERT_EQ(android::NO_ERROR, status);
+
+ lseek(fd.get(), 9, SEEK_SET);
+ uint32_t badData = 0xffffffff;
+ write(fd.get(), &badData, sizeof(uint32_t));
+ std::rewind(file);
+
+ auto retrievedTransaction = RecordedTransaction::fromFile(fd);
+
+ EXPECT_FALSE(retrievedTransaction.has_value());
+}
+
+TEST(BinderRecordedTransaction, PayloadsExceedPageBoundaries) {
+ // File contents are read with mmap.
+ // This test verifies that transactions are read from portions
+ // of files that cross page boundaries and don't start at a
+ // page boundary offset of the fd.
+ const size_t pageSize = sysconf(_SC_PAGE_SIZE);
+ const size_t largeDataSize = pageSize + 100;
+ std::vector<uint8_t> largePayload;
+ uint8_t filler = 0xaa;
+ largePayload.insert(largePayload.end(), largeDataSize, filler);
+ Parcel d;
+ d.writeInt32(12);
+ d.writeInt64(2);
+ d.writeByteVector(largePayload);
+ Parcel r;
+ r.writeInt32(99);
+ timespec ts = {1232456, 567890};
+ auto transaction = RecordedTransaction::fromDetails(1, 42, ts, d, r, 0);
+
+ auto file = std::tmpfile();
+ auto fd = unique_fd(fcntl(fileno(file), F_DUPFD, 1));
+
+ // Write to file twice
+ status_t status = transaction->dumpToFile(fd);
+ ASSERT_EQ(android::NO_ERROR, status);
+ status = transaction->dumpToFile(fd);
+ ASSERT_EQ(android::NO_ERROR, status);
+
+ std::rewind(file);
+
+ for (int i = 0; i < 2; i++) {
+ auto retrievedTransaction = RecordedTransaction::fromFile(fd);
+
+ EXPECT_EQ(retrievedTransaction->getCode(), 1);
+ EXPECT_EQ(retrievedTransaction->getFlags(), 42);
+ EXPECT_EQ(retrievedTransaction->getTimestamp().tv_sec, ts.tv_sec);
+ EXPECT_EQ(retrievedTransaction->getTimestamp().tv_nsec, ts.tv_nsec);
+ EXPECT_EQ(retrievedTransaction->getDataParcel().dataSize(), d.dataSize());
+ EXPECT_EQ(retrievedTransaction->getReplyParcel().dataSize(), 4);
+ EXPECT_EQ(retrievedTransaction->getReturnedStatus(), 0);
+ EXPECT_EQ(retrievedTransaction->getVersion(), 0);
+
+ EXPECT_EQ(retrievedTransaction->getDataParcel().readInt32(), 12);
+ EXPECT_EQ(retrievedTransaction->getDataParcel().readInt64(), 2);
+ std::optional<std::vector<uint8_t>> payloadOut;
+ EXPECT_EQ(retrievedTransaction->getDataParcel().readByteVector(&payloadOut), android::OK);
+ EXPECT_EQ(payloadOut.value(), largePayload);
+
+ EXPECT_EQ(retrievedTransaction->getReplyParcel().readInt32(), 99);
+ }
+}