Make RecordedTransaction format exensible
This patch changes the file format for RecordedTransactions towards
stable and extensibility. Warnings about stability are retained so that
the TransactionHeader may be more easily changed to serve our needs for
a "version 1".
Test: `atest binderUnitTest`
Change-Id: If8d63304815a227a8ee2bb8f7bfaa1ab33bd6e0f
diff --git a/libs/binder/BinderRecordReplay.cpp b/libs/binder/BinderRecordReplay.cpp
index 90c02a8..9463785 100644
--- a/libs/binder/BinderRecordReplay.cpp
+++ b/libs/binder/BinderRecordReplay.cpp
@@ -16,10 +16,12 @@
#include <android-base/file.h>
#include <android-base/logging.h>
+#include <android-base/unique_fd.h>
#include <binder/BinderRecordReplay.h>
#include <algorithm>
using android::Parcel;
+using android::base::borrowed_fd;
using android::base::unique_fd;
using android::binder::debug::RecordedTransaction;
@@ -30,22 +32,78 @@
static_assert(PADDING8(7) == 1);
static_assert(PADDING8(8) == 0);
-// Transactions are sequentially recorded to the file descriptor in the following format:
+// Transactions are sequentially recorded to a file descriptor.
//
-// RecordedTransaction.TransactionHeader (32 bytes)
-// Sent Parcel data (getDataSize() bytes)
-// padding (enough bytes to align the reply Parcel data to 8 bytes)
-// Reply Parcel data (getReplySize() bytes)
-// padding (enough bytes to align the next header to 8 bytes)
-// [repeats with next transaction]
+// An individual RecordedTransaction is written with the following format:
//
-// Warning: This format is non-stable
+// WARNING: Though the following format is designed to be stable and
+// extensible, it is under active development and should be considered
+// unstable until this warning is removed.
+//
+// A RecordedTransaction is written to a file as a sequence of Chunks.
+//
+// A Chunk consists of a ChunkDescriptor, Data, and Padding.
+//
+// Data and Padding may each be zero-length as specified by the
+// ChunkDescriptor.
+//
+// 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.
+//
+// ┌───────────────────────────┐
+// │Chunk │
+// │┌─────────────────────────┐│
+// ││ChunkDescriptor ││
+// ││┌───────────┬───────────┐││
+// │││chunkType │paddingSize│││
+// │││uint32_t │uint32_t ├┼┼───┐
+// ││├───────────┴───────────┤││ │
+// │││dataSize │││ │
+// │││uint64_t ├┼┼─┐ │
+// ││└───────────────────────┘││ │ │
+// │└─────────────────────────┘│ │ │
+// │┌─────────────────────────┐│ │ │
+// ││Data ││ │ │
+// ││bytes * dataSize │◀─┘ │
+// │└─────────────────────────┘│ │
+// │┌─────────────────────────┐│ │
+// ││Padding ││ │
+// ││bytes * paddingSize │◀───┘
+// │└─────────────────────────┘│
+// └───────────────────────────┘
+//
+// A RecordedTransaction is written as a Header Chunk with fields about the
+// transaction, a Data Parcel chunk, a Reply Parcel Chunk, and an End Chunk.
+// ┌──────────────────────┐
+// │ Header Chunk │
+// ├──────────────────────┤
+// │ Sent Parcel Chunk │
+// ├──────────────────────┤
+// │ Reply Parcel Chunk │
+// ├──────────────────────┤
+// ║ 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:
+//
+// 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.
+// 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.
RecordedTransaction::RecordedTransaction(RecordedTransaction&& t) noexcept {
- mHeader = {t.getCode(), t.getFlags(), t.getDataSize(),
- t.getReplySize(), t.getReturnedStatus(), t.getVersion()};
- mSent.setData(t.getDataParcel().data(), t.getDataSize());
- mReply.setData(t.getReplyParcel().data(), t.getReplySize());
+ mHeader = {t.getCode(), t.getFlags(), t.getReturnedStatus(), t.getVersion()};
+ mSent.setData(t.getDataParcel().data(), t.getDataParcel().dataSize());
+ mReply.setData(t.getReplyParcel().data(), t.getReplyParcel().dataSize());
}
std::optional<RecordedTransaction> RecordedTransaction::fromDetails(uint32_t code, uint32_t flags,
@@ -53,19 +111,15 @@
const Parcel& replyParcel,
status_t err) {
RecordedTransaction t;
- t.mHeader = {code,
- flags,
- static_cast<uint64_t>(dataParcel.dataSize()),
- static_cast<uint64_t>(replyParcel.dataSize()),
- static_cast<int32_t>(err),
+ t.mHeader = {code, flags, static_cast<int32_t>(err),
dataParcel.isForRpc() ? static_cast<uint32_t>(1) : static_cast<uint32_t>(0)};
- if (t.mSent.setData(dataParcel.data(), t.getDataSize()) != android::NO_ERROR) {
+ if (t.mSent.setData(dataParcel.data(), dataParcel.dataSize()) != android::NO_ERROR) {
LOG(INFO) << "Failed to set sent parcel data.";
return std::nullopt;
}
- if (t.mReply.setData(replyParcel.data(), t.getReplySize()) != android::NO_ERROR) {
+ if (t.mReply.setData(replyParcel.data(), replyParcel.dataSize()) != android::NO_ERROR) {
LOG(INFO) << "Failed to set reply parcel data.";
return std::nullopt;
}
@@ -73,80 +127,154 @@
return std::optional<RecordedTransaction>(std::move(t));
}
+enum {
+ HEADER_CHUNK = 0x00000001,
+ DATA_PARCEL_CHUNK = 0x00000002,
+ REPLY_PARCEL_CHUNK = 0x00000003,
+ INVALID_CHUNK = 0x00fffffe,
+ END_CHUNK = 0x00ffffff,
+};
+
+struct ChunkDescriptor {
+ uint32_t chunkType = 0;
+ uint32_t padding = 0;
+ uint32_t dataSize = 0;
+ uint32_t reserved = 0; // Future checksum
+};
+
+static android::status_t readChunkDescriptor(borrowed_fd fd, ChunkDescriptor* chunkOut) {
+ if (!android::base::ReadFully(fd, chunkOut, sizeof(ChunkDescriptor))) {
+ LOG(INFO) << "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;
+ }
+ return android::NO_ERROR;
+}
+
std::optional<RecordedTransaction> RecordedTransaction::fromFile(const unique_fd& fd) {
RecordedTransaction t;
- if (!android::base::ReadFully(fd, &t.mHeader, sizeof(mHeader))) {
- LOG(INFO) << "Failed to read transactionHeader from fd " << fd.get();
- return std::nullopt;
- }
- if (t.getVersion() != 0) {
- LOG(INFO) << "File corrupted: transaction version is not 0.";
- return std::nullopt;
- }
+ ChunkDescriptor chunk;
- std::vector<uint8_t> bytes;
- bytes.resize(t.getDataSize());
- if (!android::base::ReadFully(fd, bytes.data(), t.getDataSize())) {
- LOG(INFO) << "Failed to read sent parcel data from fd " << fd.get();
- return std::nullopt;
- }
- if (t.mSent.setData(bytes.data(), t.getDataSize()) != android::NO_ERROR) {
- LOG(INFO) << "Failed to set sent parcel data.";
- return std::nullopt;
- }
-
- uint8_t padding[7];
- if (!android::base::ReadFully(fd, padding, PADDING8(t.getDataSize()))) {
- LOG(INFO) << "Failed to read sent parcel padding from fd " << fd.get();
- return std::nullopt;
- }
- if (std::any_of(padding, padding + 7, [](uint8_t i) { return i != 0; })) {
- LOG(INFO) << "File corrupted: padding isn't 0.";
- return std::nullopt;
- }
-
- bytes.resize(t.getReplySize());
- if (!android::base::ReadFully(fd, bytes.data(), t.getReplySize())) {
- LOG(INFO) << "Failed to read reply parcel data from fd " << fd.get();
- return std::nullopt;
- }
- if (t.mReply.setData(bytes.data(), t.getReplySize()) != android::NO_ERROR) {
- LOG(INFO) << "Failed to set reply parcel data.";
- return std::nullopt;
- }
-
- if (!android::base::ReadFully(fd, padding, PADDING8(t.getReplySize()))) {
- LOG(INFO) << "Failed to read parcel padding from fd " << fd.get();
- return std::nullopt;
- }
- if (std::any_of(padding, padding + 7, [](uint8_t i) { return i != 0; })) {
- LOG(INFO) << "File corrupted: padding isn't 0.";
- return std::nullopt;
- }
+ do {
+ if (NO_ERROR != readChunkDescriptor(fd, &chunk)) {
+ LOG(INFO) << "Failed to read chunk descriptor.";
+ return std::nullopt;
+ }
+ 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) << ".";
+ 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;
+ }
+ 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();
+ 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();
+ 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;
+ }
+ } while (chunk.chunkType != END_CHUNK);
return std::optional<RecordedTransaction>(std::move(t));
}
+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;
+ }
+ // - 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;
+ }
+
+ if (!android::base::WriteFully(fd, data, byteCount)) {
+ LOG(INFO) << "Failed to write chunk data to fd " << fd.get();
+ return UNKNOWN_ERROR;
+ }
+
+ const uint8_t zeros[7] = {0};
+ if (!android::base::WriteFully(fd, zeros, additionalPaddingCount)) {
+ LOG(INFO) << "Failed to write chunk padding to fd " << fd.get();
+ return UNKNOWN_ERROR;
+ }
+ return NO_ERROR;
+}
+
android::status_t RecordedTransaction::dumpToFile(const unique_fd& fd) const {
- if (!android::base::WriteFully(fd, &mHeader, sizeof(mHeader))) {
+ 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();
return UNKNOWN_ERROR;
}
- if (!android::base::WriteFully(fd, mSent.data(), getDataSize())) {
- LOG(INFO) << "Failed to write sent parcel data to fd " << fd.get();
+ if (NO_ERROR != writeChunk(fd, DATA_PARCEL_CHUNK, mSent.dataSize(), mSent.data())) {
+ LOG(INFO) << "Failed to write sent Parcel to fd " << fd.get();
return UNKNOWN_ERROR;
}
- const uint8_t zeros[7] = {0};
- if (!android::base::WriteFully(fd, zeros, PADDING8(getDataSize()))) {
- LOG(INFO) << "Failed to write sent parcel padding to fd " << fd.get();
+ if (NO_ERROR != writeChunk(fd, REPLY_PARCEL_CHUNK, mReply.dataSize(), mReply.data())) {
+ LOG(INFO) << "Failed to write reply Parcel to fd " << fd.get();
return UNKNOWN_ERROR;
}
- if (!android::base::WriteFully(fd, mReply.data(), getReplySize())) {
- LOG(INFO) << "Failed to write reply parcel data to fd " << fd.get();
- return UNKNOWN_ERROR;
- }
- if (!android::base::WriteFully(fd, zeros, PADDING8(getReplySize()))) {
- LOG(INFO) << "Failed to write reply parcel padding to fd " << fd.get();
+ if (NO_ERROR != writeChunk(fd, END_CHUNK, 0, NULL)) {
+ LOG(INFO) << "Failed to write end chunk to fd " << fd.get();
return UNKNOWN_ERROR;
}
return NO_ERROR;
@@ -160,14 +288,6 @@
return mHeader.flags;
}
-uint64_t RecordedTransaction::getDataSize() const {
- return mHeader.dataSize;
-}
-
-uint64_t RecordedTransaction::getReplySize() const {
- return mHeader.replySize;
-}
-
int32_t RecordedTransaction::getReturnedStatus() const {
return mHeader.statusReturned;
}
diff --git a/libs/binder/include/binder/BinderRecordReplay.h b/libs/binder/include/binder/BinderRecordReplay.h
index 25ed5e5..609e5be 100644
--- a/libs/binder/include/binder/BinderRecordReplay.h
+++ b/libs/binder/include/binder/BinderRecordReplay.h
@@ -42,8 +42,6 @@
uint32_t getCode() const;
uint32_t getFlags() const;
- uint64_t getDataSize() const;
- uint64_t getReplySize() const;
int32_t getReturnedStatus() const;
uint32_t getVersion() const;
const Parcel& getDataParcel() const;
@@ -52,27 +50,24 @@
private:
RecordedTransaction() = default;
+ android::status_t writeChunk(const android::base::borrowed_fd, uint32_t chunkType,
+ size_t byteCount, const uint8_t* data) const;
+
#pragma clang diagnostic push
#pragma clang diagnostic error "-Wpadded"
struct TransactionHeader {
uint32_t code = 0;
uint32_t flags = 0;
- uint64_t dataSize = 0;
- uint64_t replySize = 0;
int32_t statusReturned = 0;
uint32_t version = 0; // !0 iff Rpc
};
#pragma clang diagnostic pop
- static_assert(sizeof(TransactionHeader) == 32);
+ static_assert(sizeof(TransactionHeader) == 16);
static_assert(sizeof(TransactionHeader) % 8 == 0);
TransactionHeader mHeader;
Parcel mSent;
Parcel mReply;
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wunused-private-field"
- uint8_t mReserved[40];
-#pragma clang diagnostic pop
};
} // namespace binder::debug
diff --git a/libs/binder/tests/binderRecordedTransactionTest.cpp b/libs/binder/tests/binderRecordedTransactionTest.cpp
index 23864e6..67e482d 100644
--- a/libs/binder/tests/binderRecordedTransactionTest.cpp
+++ b/libs/binder/tests/binderRecordedTransactionTest.cpp
@@ -42,8 +42,8 @@
EXPECT_EQ(retrievedTransaction->getCode(), 1);
EXPECT_EQ(retrievedTransaction->getFlags(), 42);
- EXPECT_EQ(retrievedTransaction->getDataSize(), 12);
- EXPECT_EQ(retrievedTransaction->getReplySize(), 4);
+ EXPECT_EQ(retrievedTransaction->getDataParcel().dataSize(), 12);
+ EXPECT_EQ(retrievedTransaction->getReplyParcel().dataSize(), 4);
EXPECT_EQ(retrievedTransaction->getReturnedStatus(), 0);
EXPECT_EQ(retrievedTransaction->getVersion(), 0);