libsnapshot: Fix premature truncation in CowWriter.

When opening in append mode, we ftruncate() the COW. This has three side
effects:
 (1) If the COW is never modified, or Finalized(), the state of the COW
     will have changed. Ideally it should only change on an explicit
     write operation.
 (2) Data after the current cluster will be accidentally thrown away.
 (3) The ending "cluster" op will be thrown away if the current cluster
     was incomplete, and thus the last valid label could be invalidated.

Bug: 183985866
Test: cow_api_test
Change-Id: I3c9a38553b7492a3d6e71d177d75ddb1b6490dfe
This commit is contained in:
David Anderson 2021-04-02 16:04:22 -07:00 committed by Daniel Rosenberg
parent 9a95ed928f
commit db25e8e32d
2 changed files with 99 additions and 4 deletions

View file

@ -25,6 +25,10 @@
#include <libsnapshot/cow_reader.h>
#include <libsnapshot/cow_writer.h>
using testing::AssertionFailure;
using testing::AssertionResult;
using testing::AssertionSuccess;
namespace android {
namespace snapshot {
@ -781,6 +785,90 @@ TEST_F(CowTest, AppendAfterFinalize) {
ASSERT_TRUE(reader.Parse(cow_->fd));
}
AssertionResult WriteDataBlock(CowWriter* writer, uint64_t new_block, std::string data) {
data.resize(writer->options().block_size, '\0');
if (!writer->AddRawBlocks(new_block, data.data(), data.size())) {
return AssertionFailure() << "Failed to add raw block";
}
return AssertionSuccess();
}
AssertionResult CompareDataBlock(CowReader* reader, const CowOperation& op,
const std::string& data) {
CowHeader header;
reader->GetHeader(&header);
std::string cmp = data;
cmp.resize(header.block_size, '\0');
StringSink sink;
if (!reader->ReadData(op, &sink)) {
return AssertionFailure() << "Failed to read data block";
}
if (cmp != sink.stream()) {
return AssertionFailure() << "Data blocks did not match, expected " << cmp << ", got "
<< sink.stream();
}
return AssertionSuccess();
}
TEST_F(CowTest, ResumeMidCluster) {
CowOptions options;
options.cluster_ops = 7;
auto writer = std::make_unique<CowWriter>(options);
ASSERT_TRUE(writer->Initialize(cow_->fd));
ASSERT_TRUE(WriteDataBlock(writer.get(), 1, "Block 1"));
ASSERT_TRUE(WriteDataBlock(writer.get(), 2, "Block 2"));
ASSERT_TRUE(WriteDataBlock(writer.get(), 3, "Block 3"));
ASSERT_TRUE(writer->AddLabel(1));
ASSERT_TRUE(writer->Finalize());
ASSERT_TRUE(WriteDataBlock(writer.get(), 4, "Block 4"));
ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);
writer = std::make_unique<CowWriter>(options);
ASSERT_TRUE(writer->InitializeAppend(cow_->fd, 1));
ASSERT_TRUE(WriteDataBlock(writer.get(), 4, "Block 4"));
ASSERT_TRUE(WriteDataBlock(writer.get(), 5, "Block 5"));
ASSERT_TRUE(WriteDataBlock(writer.get(), 6, "Block 6"));
ASSERT_TRUE(WriteDataBlock(writer.get(), 7, "Block 7"));
ASSERT_TRUE(WriteDataBlock(writer.get(), 8, "Block 8"));
ASSERT_TRUE(writer->AddLabel(2));
ASSERT_TRUE(writer->Finalize());
ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);
CowReader reader;
ASSERT_TRUE(reader.Parse(cow_->fd));
auto iter = reader.GetOpIter();
size_t num_replace = 0;
size_t max_in_cluster = 0;
size_t num_in_cluster = 0;
size_t num_clusters = 0;
while (!iter->Done()) {
const auto& op = iter->Get();
num_in_cluster++;
max_in_cluster = std::max(max_in_cluster, num_in_cluster);
if (op.type == kCowReplaceOp) {
num_replace++;
ASSERT_EQ(op.new_block, num_replace);
ASSERT_TRUE(CompareDataBlock(&reader, op, "Block " + std::to_string(num_replace)));
} else if (op.type == kCowClusterOp) {
num_in_cluster = 0;
num_clusters++;
}
iter->Next();
}
ASSERT_EQ(num_replace, 8);
ASSERT_EQ(max_in_cluster, 7);
ASSERT_EQ(num_clusters, 2);
}
} // namespace snapshot
} // namespace android

View file

@ -232,10 +232,6 @@ bool CowWriter::OpenForAppend(uint64_t label) {
// Free reader so we own the descriptor position again.
reader = nullptr;
// Remove excess data
if (!Truncate(next_op_pos_)) {
return false;
}
if (lseek(fd_.get(), next_op_pos_, SEEK_SET) < 0) {
PLOG(ERROR) << "lseek failed";
return false;
@ -403,6 +399,17 @@ bool CowWriter::Finalize() {
return false;
}
// Remove excess data, if we're in append mode and threw away more data
// than we wrote before.
off_t offs = lseek(fd_.get(), 0, SEEK_CUR);
if (offs < 0) {
PLOG(ERROR) << "Failed to lseek to find current position";
return false;
}
if (!Truncate(offs)) {
return false;
}
// Reposition for additional Writing
if (extra_cluster) {
current_cluster_size_ = continue_cluster_size;