third_party.pigweed.src/pw_software_update/bundled_update_service.cc
Eli Lipsitz 872bb2b150 pw_software_update: Fix recursive lock acquisition in nanopb service
This commit fixes the same recursive lock acquisition as
I0ee7994fa5e093a05e349876406d0a2bd89fc3e7, in the nanopb software update
service.

The NotifyTransferSucceeded method acquires the same lock, leading to a
recursive lock acquisition. This commit fixes the issue by
by moving the outer lock acquisition into
the if statement that calls SET_ERROR, which is mutually exclusive with
the call to NotifyTransferSucceeded.

Change-Id: I30db9cc47ceca8af5b724018ee8b29cef6fc58cd
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/112114
Pigweed-Auto-Submit: Eli Lipsitz <elipsitz@google.com>
Reviewed-by: Ali Zhang <alizhang@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
2022-09-27 00:31:52 +00:00

540 lines
20 KiB
C++

// Copyright 2021 The Pigweed Authors
//
// 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
//
// https://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.
#define PW_LOG_MODULE_NAME "PWSU"
#define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
#include "pw_software_update/bundled_update_service.h"
#include <mutex>
#include <string_view>
#include "pw_log/log.h"
#include "pw_result/result.h"
#include "pw_software_update/config.h"
#include "pw_software_update/manifest_accessor.h"
#include "pw_software_update/update_bundle.pwpb.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_status/try.h"
#include "pw_string/string_builder.h"
#include "pw_string/util.h"
#include "pw_sync/borrow.h"
#include "pw_sync/mutex.h"
#include "pw_tokenizer/tokenize.h"
namespace pw::software_update {
namespace {
using BorrowedStatus =
sync::BorrowedPointer<pw_software_update_BundledUpdateStatus, sync::Mutex>;
using BundledUpdateState = pw_software_update_BundledUpdateState_Enum;
using BundledUpdateStatus = pw_software_update_BundledUpdateStatus;
// TODO(keir): Convert all the CHECKs in the RPC service to gracefully report
// errors.
#define SET_ERROR(res, message, ...) \
do { \
PW_LOG_ERROR(message, __VA_ARGS__); \
if (!IsFinished()) { \
Finish(res); \
{ \
BorrowedStatus borrowed_status = status_.acquire(); \
size_t note_size = sizeof(borrowed_status->note.bytes); \
PW_TOKENIZE_TO_BUFFER( \
borrowed_status->note.bytes, &(note_size), message, __VA_ARGS__); \
borrowed_status->note.size = note_size; \
borrowed_status->has_note = true; \
} \
} \
} while (false)
} // namespace
Status BundledUpdateService::GetStatus(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
response = *status_.acquire();
return OkStatus();
}
Status BundledUpdateService::Start(
const pw_software_update_StartRequest& request,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
// Check preconditions.
const BundledUpdateState state = status_.acquire()->state;
if (state != pw_software_update_BundledUpdateState_Enum_INACTIVE) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"Start() can only be called from INACTIVE state. "
"Current state: %d. Abort() then Reset() must be called first",
static_cast<int>(state));
response = *status_.acquire();
return Status::FailedPrecondition();
}
{
BorrowedStatus borrowed_status = status_.acquire();
PW_DCHECK(!borrowed_status->has_transfer_id);
PW_DCHECK(!borrowed_status->has_result);
PW_DCHECK(borrowed_status->current_state_progress_hundreth_percent == 0);
PW_DCHECK(borrowed_status->bundle_filename[0] == '\0');
PW_DCHECK(borrowed_status->note.size == 0);
}
// Notify the backend of pending transfer.
if (const Status status = backend_.BeforeUpdateStart(); !status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"Backend error on BeforeUpdateStart()");
response = *status_.acquire();
return status;
}
// Enable bundle transfer.
Result<uint32_t> possible_transfer_id =
backend_.EnableBundleTransferHandler(string::ClampedCString(
request.bundle_filename, sizeof(request.bundle_filename)));
if (!possible_transfer_id.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_TRANSFER_FAILED,
"Couldn't enable bundle transfer");
response = *status_.acquire();
return possible_transfer_id.status();
}
// Update state.
{
BorrowedStatus borrowed_status = status_.acquire();
borrowed_status->transfer_id = possible_transfer_id.value();
borrowed_status->has_transfer_id = true;
if (request.has_bundle_filename) {
const StatusWithSize sws =
string::Copy(request.bundle_filename,
borrowed_status->bundle_filename,
sizeof(borrowed_status->bundle_filename));
PW_DCHECK_OK(sws.status(),
"bundle_filename options max_sizes do not match");
borrowed_status->has_bundle_filename = true;
}
borrowed_status->state =
pw_software_update_BundledUpdateState_Enum_TRANSFERRING;
response = *borrowed_status;
}
return OkStatus();
}
Status BundledUpdateService::SetTransferred(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
const BundledUpdateState state = status_.acquire()->state;
if (state != pw_software_update_BundledUpdateState_Enum_TRANSFERRING &&
state != pw_software_update_BundledUpdateState_Enum_INACTIVE) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"SetTransferred() can only be called from TRANSFERRING or "
"INACTIVE state. State: %d",
static_cast<int>(state));
response = *status_.acquire();
return OkStatus();
}
NotifyTransferSucceeded();
response = *status_.acquire();
return OkStatus();
}
// TODO: Check for "ABORTING" state and bail if it's set.
void BundledUpdateService::DoVerify() {
std::lock_guard guard(mutex_);
const BundledUpdateState state = status_.acquire()->state;
if (state == pw_software_update_BundledUpdateState_Enum_VERIFIED) {
return; // Already done!
}
// Ensure we're in the right state.
if (state != pw_software_update_BundledUpdateState_Enum_TRANSFERRED) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"DoVerify() must be called from TRANSFERRED state. State: %d",
static_cast<int>(state));
return;
}
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_VERIFYING;
// Notify backend about pending verify.
if (const Status status = backend_.BeforeBundleVerify(); !status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Backend::BeforeBundleVerify() failed");
return;
}
// Do the actual verify.
Status status = bundle_.OpenAndVerify();
if (!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Bundle::OpenAndVerify() failed");
return;
}
bundle_open_ = true;
// Have the backend verify the user_manifest if present.
if (!backend_.VerifyManifest(bundle_.GetManifest()).ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Backend::VerifyUserManifest() failed");
return;
}
// Notify backend we're done verifying.
status = backend_.AfterBundleVerified();
if (!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Backend::AfterBundleVerified() failed");
return;
}
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_VERIFIED;
}
Status BundledUpdateService::Verify(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
// Already done? Bail.
if (state == pw_software_update_BundledUpdateState_Enum_VERIFIED) {
PW_LOG_DEBUG("Skipping verify since already verified");
return OkStatus();
}
// TODO: Remove the transferring permitted state here ASAP.
// Ensure we're in the right state.
if ((state != pw_software_update_BundledUpdateState_Enum_TRANSFERRING) &&
(state != pw_software_update_BundledUpdateState_Enum_TRANSFERRED)) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Verify() must be called from TRANSFERRED state. State: %d",
static_cast<int>(state));
response = *status_.acquire();
return Status::FailedPrecondition();
}
// TODO: We should probably make this mode idempotent.
// Already doing what was asked? Bail.
if (work_enqueued_) {
PW_LOG_DEBUG("Verification is already active");
return OkStatus();
}
// The backend's ApplyReboot as part of DoApply() shall be configured
// such that this RPC can send out the reply before the device reboots.
const Status status = work_queue_.PushWork([this] {
{
std::lock_guard y_lock(this->mutex_);
PW_DCHECK(this->work_enqueued_);
}
this->DoVerify();
{
std::lock_guard y_lock(this->mutex_);
this->work_enqueued_ = false;
}
});
if (!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Unable to equeue apply to work queue");
response = *status_.acquire();
return status;
}
work_enqueued_ = true;
response = *status_.acquire();
return OkStatus();
}
Status BundledUpdateService::Apply(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
// We do not wait to go into a finished error state if we're already
// applying, instead just let them know that yes we are working on it --
// hold on.
if (state == pw_software_update_BundledUpdateState_Enum_APPLYING) {
PW_LOG_DEBUG("Apply is already active");
return OkStatus();
}
if ((state != pw_software_update_BundledUpdateState_Enum_TRANSFERRED) &&
(state != pw_software_update_BundledUpdateState_Enum_VERIFIED)) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Apply() must be called from TRANSFERRED or VERIFIED state. "
"State: %d",
static_cast<int>(state));
return Status::FailedPrecondition();
}
// TODO: We should probably make these all idempotent properly.
if (work_enqueued_) {
PW_LOG_DEBUG("Apply is already active");
return OkStatus();
}
// The backend's ApplyReboot as part of DoApply() shall be configured
// such that this RPC can send out the reply before the device reboots.
const Status status = work_queue_.PushWork([this] {
{
std::lock_guard y_lock(this->mutex_);
PW_DCHECK(this->work_enqueued_);
}
// Error reporting is handled in DoVerify and DoApply.
this->DoVerify();
this->DoApply();
{
std::lock_guard y_lock(this->mutex_);
this->work_enqueued_ = false;
}
});
if (!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Unable to equeue apply to work queue");
response = *status_.acquire();
return status;
}
work_enqueued_ = true;
return OkStatus();
}
void BundledUpdateService::DoApply() {
std::lock_guard guard(mutex_);
const BundledUpdateState state = status_.acquire()->state;
PW_LOG_DEBUG("Attempting to apply the update");
if (state != pw_software_update_BundledUpdateState_Enum_VERIFIED) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Apply() must be called from VERIFIED state. State: %d",
static_cast<int>(state));
return;
}
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_APPLYING;
if (const Status status = backend_.BeforeApply(); !status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"BeforeApply() returned unsuccessful result: %d",
static_cast<int>(status.code()));
return;
}
// In order to report apply progress, quickly scan to see how many bytes
// will be applied.
Result<uint64_t> total_payload_bytes = bundle_.GetTotalPayloadSize();
PW_CHECK_OK(total_payload_bytes.status());
size_t target_file_bytes_to_apply =
static_cast<size_t>(total_payload_bytes.value());
protobuf::RepeatedMessages target_files =
bundle_.GetManifest().GetTargetFiles();
PW_CHECK_OK(target_files.status());
size_t target_file_bytes_applied = 0;
for (pw::protobuf::Message file_name : target_files) {
std::array<std::byte, MAX_TARGET_NAME_LENGTH> buf = {};
protobuf::String name = file_name.AsString(static_cast<uint32_t>(
pw::software_update::TargetFile::Fields::FILE_NAME));
PW_CHECK_OK(name.status());
const Result<ByteSpan> read_result = name.GetBytesReader().Read(buf);
PW_CHECK_OK(read_result.status());
const ConstByteSpan file_name_span = read_result.value();
const std::string_view file_name_view(
reinterpret_cast<const char*>(file_name_span.data()),
file_name_span.size_bytes());
if (file_name_view.compare(kUserManifestTargetFileName) == 0) {
continue; // user_manifest is not applied by the backend.
}
// Try to get an IntervalReader for the current file.
stream::IntervalReader file_reader =
bundle_.GetTargetPayload(file_name_view);
if (file_reader.status().IsNotFound()) {
PW_LOG_INFO(
"Contents of file %s missing from bundle; ignoring",
pw::MakeString<MAX_TARGET_NAME_LENGTH>(file_name_view).c_str());
continue;
}
if (!file_reader.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Could not open contents of file %s from bundle; "
"aborting update apply phase",
MakeString<MAX_TARGET_NAME_LENGTH>(file_name_view).c_str());
return;
}
const size_t bundle_offset = file_reader.start();
if (const Status status = backend_.ApplyTargetFile(
file_name_view, file_reader, bundle_offset);
!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Failed to apply target file: %d",
static_cast<int>(status.code()));
return;
}
target_file_bytes_applied += file_reader.interval_size();
const uint32_t progress_hundreth_percent =
(static_cast<uint64_t>(target_file_bytes_applied) * 100 * 100) /
target_file_bytes_to_apply;
PW_LOG_DEBUG("Apply progress: %zu/%zu Bytes (%ld%%)",
target_file_bytes_applied,
target_file_bytes_to_apply,
static_cast<unsigned long>(progress_hundreth_percent / 100));
{
BorrowedStatus borrowed_status = status_.acquire();
borrowed_status->current_state_progress_hundreth_percent =
progress_hundreth_percent;
borrowed_status->has_current_state_progress_hundreth_percent = true;
}
}
// TODO(davidrogers): Add new APPLY_REBOOTING to distinguish between pre and
// post reboot.
// Finalize the apply.
if (const Status status = backend_.ApplyReboot(); !status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Failed to do the apply reboot: %d",
static_cast<int>(status.code()));
return;
}
// TODO(davidrogers): Move this to MaybeFinishApply() once available.
Finish(pw_software_update_BundledUpdateResult_Enum_SUCCESS);
}
Status BundledUpdateService::Abort(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
if (state == pw_software_update_BundledUpdateState_Enum_APPLYING) {
return Status::FailedPrecondition();
}
if (state == pw_software_update_BundledUpdateState_Enum_INACTIVE ||
state == pw_software_update_BundledUpdateState_Enum_FINISHED) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"Tried to abort when already INACTIVE or FINISHED");
return Status::FailedPrecondition();
}
// TODO: Switch abort to async; this state change isn't externally visible.
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_ABORTING;
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_ABORTED,
"Update abort requested");
response = *status_.acquire();
return OkStatus();
}
Status BundledUpdateService::Reset(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
if (state == pw_software_update_BundledUpdateState_Enum_INACTIVE) {
return OkStatus(); // Already done.
}
if (state != pw_software_update_BundledUpdateState_Enum_FINISHED) {
SET_ERROR(
pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"Reset() must be called from FINISHED or INACTIVE state. State: %d",
static_cast<int>(state));
response = *status_.acquire();
return Status::FailedPrecondition();
}
{
*status_.acquire() = {
.state = pw_software_update_BundledUpdateState_Enum_INACTIVE};
}
// Reset the bundle.
if (bundle_open_) {
// TODO: Revisit whether this is recoverable; maybe eliminate CHECK.
PW_CHECK_OK(bundle_.Close());
bundle_open_ = false;
}
response = *status_.acquire();
return OkStatus();
}
void BundledUpdateService::NotifyTransferSucceeded() {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
if (state != pw_software_update_BundledUpdateState_Enum_TRANSFERRING) {
// This can happen if the update gets Abort()'d during the transfer and
// the transfer completes successfully.
PW_LOG_WARN(
"Got transfer succeeded notification when not in TRANSFERRING state. "
"State: %d",
static_cast<int>(state));
}
const bool transfer_ongoing = status_.acquire()->has_transfer_id;
if (transfer_ongoing) {
backend_.DisableBundleTransferHandler();
status_.acquire()->has_transfer_id = false;
} else {
PW_LOG_WARN("No ongoing transfer found, forcefully set TRANSFERRED.");
}
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_TRANSFERRED;
}
void BundledUpdateService::Finish(
pw_software_update_BundledUpdateResult_Enum result) {
if (result == pw_software_update_BundledUpdateResult_Enum_SUCCESS) {
BorrowedStatus borrowed_status = status_.acquire();
borrowed_status->current_state_progress_hundreth_percent = 0;
borrowed_status->has_current_state_progress_hundreth_percent = false;
} else {
// In the case of error, notify backend that we're about to abort the
// software update.
PW_CHECK_OK(backend_.BeforeUpdateAbort());
}
// Turn down the transfer if one is in progress.
const bool transfer_ongoing = status_.acquire()->has_transfer_id;
if (transfer_ongoing) {
backend_.DisableBundleTransferHandler();
}
status_.acquire()->has_transfer_id = false;
// Close out any open bundles.
if (bundle_open_) {
// TODO: Revisit this check; may be able to recover.
PW_CHECK_OK(bundle_.Close());
bundle_open_ = false;
}
{
BorrowedStatus borrowed_status = status_.acquire();
borrowed_status->state =
pw_software_update_BundledUpdateState_Enum_FINISHED;
borrowed_status->result = result;
borrowed_status->has_result = true;
}
}
} // namespace pw::software_update