//
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "update_engine/payload_consumer/filesystem_verifier_action.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <algorithm>
#include <cstdlib>
#include <string>
#include <base/bind.h>
#include <brillo/streams/file_stream.h>
#include "update_engine/common/boot_control_interface.h"
#include "update_engine/common/utils.h"
#include "update_engine/payload_consumer/delta_performer.h"
#include "update_engine/payload_consumer/payload_constants.h"
using std::string;
namespace chromeos_update_engine {
namespace {
const off_t kReadFileBufferSize = 128 * 1024;
} // namespace
FilesystemVerifierAction::FilesystemVerifierAction(
const BootControlInterface* boot_control,
VerifierMode verifier_mode)
: verifier_mode_(verifier_mode),
boot_control_(boot_control) {}
void FilesystemVerifierAction::PerformAction() {
// Will tell the ActionProcessor we've failed if we return.
ScopedActionCompleter abort_action_completer(processor_, this);
if (!HasInputObject()) {
LOG(ERROR) << "FilesystemVerifierAction missing input object.";
return;
}
install_plan_ = GetInputObject();
// For delta updates (major version 1) we need to populate the source
// partition hash if not pre-populated.
if (install_plan_.payload_type == InstallPayloadType::kDelta &&
install_plan_.partitions.empty() &&
verifier_mode_ == VerifierMode::kComputeSourceHash &&
DeltaPerformer::kSupportedMinorPayloadVersion <
kOpSrcHashMinorPayloadVersion) {
LOG(INFO) << "Using legacy partition names.";
InstallPlan::Partition part;
string part_path;
part.name = kLegacyPartitionNameRoot;
if (!boot_control_->GetPartitionDevice(
part.name, install_plan_.source_slot, &part_path))
return;
int block_count = 0, block_size = 0;
if (utils::GetFilesystemSize(part_path, &block_count, &block_size)) {
part.source_size = static_cast<int64_t>(block_count) * block_size;
LOG(INFO) << "Partition " << part.name << " size: " << part.source_size
<< " bytes (" << block_count << "x" << block_size << ").";
}
install_plan_.partitions.push_back(part);
part.name = kLegacyPartitionNameKernel;
if (!boot_control_->GetPartitionDevice(
part.name, install_plan_.source_slot, &part_path))
return;
off_t kernel_part_size = utils::FileSize(part_path);
if (kernel_part_size < 0)
return;
LOG(INFO) << "Partition " << part.name << " size: " << kernel_part_size
<< " bytes.";
part.source_size = kernel_part_size;
install_plan_.partitions.push_back(part);
}
if (install_plan_.partitions.empty()) {
LOG(INFO) << "No partitions to verify.";
if (HasOutputPipe())
SetOutputObject(install_plan_);
abort_action_completer.set_code(ErrorCode::kSuccess);
return;
}
StartPartitionHashing();
abort_action_completer.set_should_complete(false);
}
void FilesystemVerifierAction::TerminateProcessing() {
cancelled_ = true;
Cleanup(ErrorCode::kSuccess); // error code is ignored if canceled_ is true.
}
bool FilesystemVerifierAction::IsCleanupPending() const {
return src_stream_ != nullptr;
}
void FilesystemVerifierAction::Cleanup(ErrorCode code) {
src_stream_.reset();
// This memory is not used anymore.
buffer_.clear();
if (cancelled_)
return;
if (code == ErrorCode::kSuccess && HasOutputPipe())
SetOutputObject(install_plan_);
processor_->ActionComplete(this, code);
}
void FilesystemVerifierAction::StartPartitionHashing() {
if (partition_index_ == install_plan_.partitions.size()) {
// We never called this action with kVerifySourceHash directly, if we are in
// this mode, it means the target partition verification has failed, so we
// should set the error code to reflect the error in target.
if (verifier_mode_ == VerifierMode::kVerifySourceHash)
Cleanup(ErrorCode::kNewRootfsVerificationError);
else
Cleanup(ErrorCode::kSuccess);
return;
}
InstallPlan::Partition& partition =
install_plan_.partitions[partition_index_];
string part_path;
switch (verifier_mode_) {
case VerifierMode::kComputeSourceHash:
case VerifierMode::kVerifySourceHash:
boot_control_->GetPartitionDevice(
partition.name, install_plan_.source_slot, &part_path);
remaining_size_ = partition.source_size;
break;
case VerifierMode::kVerifyTargetHash:
boot_control_->GetPartitionDevice(
partition.name, install_plan_.target_slot, &part_path);
remaining_size_ = partition.target_size;
break;
}
LOG(INFO) << "Hashing partition " << partition_index_ << " ("
<< partition.name << ") on device " << part_path;
if (part_path.empty())
return Cleanup(ErrorCode::kFilesystemVerifierError);
brillo::ErrorPtr error;
src_stream_ = brillo::FileStream::Open(
base::FilePath(part_path),
brillo::Stream::AccessMode::READ,
brillo::FileStream::Disposition::OPEN_EXISTING,
&error);
if (!src_stream_) {
LOG(ERROR) << "Unable to open " << part_path << " for reading";
return Cleanup(ErrorCode::kFilesystemVerifierError);
}
buffer_.resize(kReadFileBufferSize);
read_done_ = false;
hasher_.reset(new HashCalculator());
// Start the first read.
ScheduleRead();
}
void FilesystemVerifierAction::ScheduleRead() {
size_t bytes_to_read = std::min(static_cast<int64_t>(buffer_.size()),
remaining_size_);
if (!bytes_to_read) {
OnReadDoneCallback(0);
return;
}
bool read_async_ok = src_stream_->ReadAsync(
buffer_.data(),
bytes_to_read,
base::Bind(&FilesystemVerifierAction::OnReadDoneCallback,
base::Unretained(this)),
base::Bind(&FilesystemVerifierAction::OnReadErrorCallback,
base::Unretained(this)),
nullptr);
if (!read_async_ok) {
LOG(ERROR) << "Unable to schedule an asynchronous read from the stream.";
Cleanup(ErrorCode::kError);
}
}
void FilesystemVerifierAction::OnReadDoneCallback(size_t bytes_read) {
if (bytes_read == 0) {
read_done_ = true;
} else {
remaining_size_ -= bytes_read;
CHECK(!read_done_);
if (!hasher_->Update(buffer_.data(), bytes_read)) {
LOG(ERROR) << "Unable to update the hash.";
Cleanup(ErrorCode::kError);
return;
}
}
// We either terminate the current partition or have more data to read.
if (cancelled_)
return Cleanup(ErrorCode::kError);
if (read_done_ || remaining_size_ == 0) {
if (remaining_size_ != 0) {
LOG(ERROR) << "Failed to read the remaining " << remaining_size_
<< " bytes from partition "
<< install_plan_.partitions[partition_index_].name;
return Cleanup(ErrorCode::kFilesystemVerifierError);
}
return FinishPartitionHashing();
}
ScheduleRead();
}
void FilesystemVerifierAction::OnReadErrorCallback(
const brillo::Error* error) {
// TODO(deymo): Transform the read-error into an specific ErrorCode.
LOG(ERROR) << "Asynchronous read failed.";
Cleanup(ErrorCode::kError);
}
void FilesystemVerifierAction::FinishPartitionHashing() {
if (!hasher_->Finalize()) {
LOG(ERROR) << "Unable to finalize the hash.";
return Cleanup(ErrorCode::kError);
}
InstallPlan::Partition& partition =
install_plan_.partitions[partition_index_];
LOG(INFO) << "Hash of " << partition.name << ": " << hasher_->hash();
switch (verifier_mode_) {
case VerifierMode::kComputeSourceHash:
partition.source_hash = hasher_->raw_hash();
partition_index_++;
break;
case VerifierMode::kVerifyTargetHash:
if (partition.target_hash != hasher_->raw_hash()) {
LOG(ERROR) << "New '" << partition.name
<< "' partition verification failed.";
if (DeltaPerformer::kSupportedMinorPayloadVersion <
kOpSrcHashMinorPayloadVersion)
return Cleanup(ErrorCode::kNewRootfsVerificationError);
// If we support per-operation source hash, then we skipped source
// filesystem verification, now that the target partition does not
// match, we need to switch to kVerifySourceHash mode to check if it's
// because the source partition does not match either.
verifier_mode_ = VerifierMode::kVerifySourceHash;
partition_index_ = 0;
} else {
partition_index_++;
}
break;
case VerifierMode::kVerifySourceHash:
if (partition.source_hash != hasher_->raw_hash()) {
LOG(ERROR) << "Old '" << partition.name
<< "' partition verification failed.";
return Cleanup(ErrorCode::kDownloadStateInitializationError);
}
partition_index_++;
break;
}
// Start hashing the next partition, if any.
hasher_.reset();
buffer_.clear();
src_stream_->CloseBlocking(nullptr);
StartPartitionHashing();
}
} // namespace chromeos_update_engine