/* * Copyright 2018 Google Inc. * * 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 "firmware_handler.hpp" #include "data.hpp" #include "flags.hpp" #include "image_handler.hpp" #include "status.hpp" #include "util.hpp" #include #include #include #include #include #include #include #include namespace ipmi_flash { std::unique_ptr FirmwareBlobHandler::CreateFirmwareBlobHandler( std::vector&& firmwares, std::vector&& transports, ActionMap&& actionPacks) { /* There must be at least one in addition to the hash blob handler. */ if (firmwares.size() < 2) { std::fprintf(stderr, "Must provide at least two firmware handlers."); return nullptr; } if (transports.empty()) { return nullptr; } if (actionPacks.empty()) { return nullptr; } std::vector blobs; blobs.reserve(firmwares.size()); std::for_each(firmwares.begin(), firmwares.end(), [&blobs](const auto& blob) { blobs.emplace_back(blob.blobName); }); if (0 == std::count(blobs.begin(), blobs.end(), hashBlobId)) { return nullptr; } return std::make_unique( std::move(firmwares), blobs, std::move(transports), std::move(actionPacks)); } /* Check if the path is in our supported list (or active list). */ bool FirmwareBlobHandler::canHandleBlob(const std::string& path) { return (std::count(blobIDs.begin(), blobIDs.end(), path) > 0); } /* * Grab the list of supported firmware. * * If there's an open firmware session, it'll already be present in the * list as "/flash/active/image", and if the hash has started, * "/flash/active/hash" regardless of mechanism. This is done in the open * command, no extra work is required here. */ std::vector FirmwareBlobHandler::getBlobIds() { return blobIDs; } /* * Per the design, this mean abort, and this will trigger whatever * appropriate actions are required to abort the process. */ bool FirmwareBlobHandler::deleteBlob(const std::string&) { switch (state) { case UpdateState::notYetStarted: /* Trying to delete anything at this point has no effect and returns * false. */ return false; case UpdateState::verificationPending: abortProcess(); return true; case UpdateState::updatePending: abortProcess(); return true; default: break; } return false; } /* * Stat on the files will return information such as what supported * transport mechanisms are available. * * Stat on an active file or hash will return information such as the size * of the data cached, and any additional pertinent information. The * blob_state on the active files will return the state of the update. */ bool FirmwareBlobHandler::stat(const std::string& path, blobs::BlobMeta* meta) { /* We know we support this path because canHandle is called ahead */ if (path == verifyBlobId || path == activeImageBlobId || path == activeHashBlobId || path == updateBlobId) { /* These blobs are placeholders that indicate things, or allow actions, * but are not stat-able as-is. */ return false; } /* They are requesting information about the generic blob_id. */ /* Older host tools expect the blobState to contain a bitmask of available * transport backends, so report that we support all of them in order to * preserve backwards compatibility. */ meta->blobState = transportMask; meta->size = 0; return true; } ActionStatus FirmwareBlobHandler::getActionStatus() { ActionStatus value = ActionStatus::unknown; auto* pack = getActionPack(); switch (state) { case UpdateState::verificationPending: value = ActionStatus::unknown; break; case UpdateState::verificationStarted: /* If we got here, there must be data AND a hash, not just a hash, * therefore pack will be known. */ if (!pack) { break; } value = pack->verification->status(); lastVerificationStatus = value; break; case UpdateState::verificationCompleted: value = lastVerificationStatus; break; case UpdateState::updatePending: value = ActionStatus::unknown; break; case UpdateState::updateStarted: if (!pack) { break; } value = pack->update->status(); lastUpdateStatus = value; break; case UpdateState::updateCompleted: value = lastUpdateStatus; break; default: break; } return value; } /* * Return stat information on an open session. It therefore must be an active * handle to either the active image or active hash. */ bool FirmwareBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta) { auto item = lookup.find(session); if (item == lookup.end()) { return false; } /* The size here refers to the size of the file -- of something analogous. */ meta->size = (item->second->imageHandler) ? item->second->imageHandler->getSize() : 0; meta->metadata.clear(); if (item->second->activePath == verifyBlobId || item->second->activePath == updateBlobId) { ActionStatus value = getActionStatus(); meta->metadata.push_back(static_cast(value)); /* Change the firmware handler's state and the blob's stat value * depending. */ if (value == ActionStatus::success || value == ActionStatus::failed) { if (item->second->activePath == verifyBlobId) { changeState(UpdateState::verificationCompleted); } else { /* item->second->activePath == updateBlobId */ changeState(UpdateState::updateCompleted); } item->second->flags &= ~blobs::StateFlags::committing; if (value == ActionStatus::success) { item->second->flags |= blobs::StateFlags::committed; } else { item->second->flags |= blobs::StateFlags::commit_error; } } } /* The blobState here relates to an active session, so we should return the * flags used to open this session. */ meta->blobState = item->second->flags; /* The metadata blob returned comes from the data handler... it's used for * instance, in P2A bridging to get required information about the mapping, * and is the "opposite" of the lpc writemeta requirement. */ if (item->second->dataHandler) { auto bytes = item->second->dataHandler->readMeta(); meta->metadata.insert(meta->metadata.begin(), bytes.begin(), bytes.end()); } return true; } /* * If you open /flash/image or /flash/tarball, or /flash/hash it will * interpret the open flags and perform whatever actions are required for * that update process. The session returned can be used immediately for * sending data down, without requiring one to open the new active file. * * If you open the active flash image or active hash it will let you * overwrite pieces, depending on the state. * * Once the verification process has started the active files cannot be * opened. * * You can only have one open session at a time. Which means, you can only * have one file open at a time. Trying to open the hash blob_id while you * still have the flash image blob_id open will fail. Opening the flash * blob_id when it is already open will fail. */ bool FirmwareBlobHandler::open(uint16_t session, uint16_t flags, const std::string& path) { /* Is there an open session already? We only allow one at a time. * * Further on this, if there's an active session to the hash we don't allow * re-opening the image, and if we have the image open, we don't allow * opening the hash. This design decision may be re-evaluated, and changed * to only allow one session per object type (of the two types). But, * consider if the hash is open, do we want to allow writing to the image? * And why would we? But, really, the point of no-return is once the * verification process has begun -- which is done via commit() on the hash * blob_id, we no longer want to allow updating the contents. */ if (fileOpen()) { return false; } /* The active blobs are only meant to indicate status that something has * opened the image file or the hash file. */ if (path == activeImageBlobId || path == activeHashBlobId) { /* 2a) are they opening the active image? this can only happen if they * already started one (due to canHandleBlob's behavior). */ /* 2b) are they opening the active hash? this can only happen if they * already started one (due to canHandleBlob's behavior). */ return false; } /* Check that they've opened for writing - read back not currently * supported. */ if ((flags & blobs::OpenFlags::write) == 0) { return false; } /* Because canHandleBlob is called before open, we know that if they try to * open the verifyBlobId, they're in a state where it's present. */ switch (state) { case UpdateState::notYetStarted: /* Only hashBlobId and firmware BlobIds present. */ break; case UpdateState::uploadInProgress: /* Unreachable code because if it's started a file is open. */ break; case UpdateState::verificationPending: /* Handle opening the verifyBlobId --> we know the image and hash * aren't open because of the fileOpen() check. They can still open * other files from this state to transition back into * uploadInProgress. * * The file must be opened for writing, but no transport mechanism * specified since it's irrelevant. */ if (path == verifyBlobId) { verifyImage.flags = flags; lookup[session] = &verifyImage; return true; } break; case UpdateState::verificationStarted: case UpdateState::verificationCompleted: /* Unreachable code because if it's started a file is open. */ return false; case UpdateState::updatePending: { /* When in this state, they can only open the updateBlobId */ if (path == updateBlobId) { updateImage.flags = flags; lookup[session] = &updateImage; return true; } else { return false; } } case UpdateState::updateStarted: case UpdateState::updateCompleted: /* Unreachable code because if it's started a file is open. */ break; default: break; } /* To support multiple firmware options, we need to make sure they're * opening the one they already opened during this update sequence, or it's * the first time they're opening it. */ if (path != hashBlobId) { /* If they're not opening the hashBlobId they must be opening a firmware * handler. */ if (openedFirmwareType.empty()) { /* First time for this sequence. */ openedFirmwareType = path; } else { if (openedFirmwareType != path) { /* Previously, in this sequence they opened /flash/image, and * now they're opening /flash/bios without finishing out * /flash/image (for example). */ std::fprintf(stderr, "Trying to open alternate firmware while " "unfinished with other firmware.\n"); return false; } } } /* There are two abstractions at play, how you get the data and how you * handle that data. such that, whether the data comes from the PCI bridge * or LPC bridge is not connected to whether the data goes into a static * layout flash update or a UBI tarball. */ std::uint16_t transportFlag = flags & transportMask; /* How are they expecting to copy this data? */ auto d = std::find_if(transports.begin(), transports.end(), [&transportFlag](const auto& iter) { return (iter.bitmask == transportFlag); }); if (d == transports.end()) { return false; } /* We found the transport handler they requested */ /* Elsewhere I do this check by checking "if ::ipmi" because that's the * only non-external data pathway -- but this is just a more generic * approach to that. */ if (d->handler) { /* If the data handler open call fails, open fails. */ if (!d->handler->open()) { return false; } } /* Do we have a file handler for the type of file they're opening. * Note: This should only fail if something is somehow crazy wrong. * Since the canHandle() said yes, and that's tied into the list of explicit * firmware handers (and file handlers, like this'll know where to write the * tarball, etc). */ auto h = std::find_if( handlers.begin(), handlers.end(), [&path](const auto& iter) { return (iter.blobName == path); }); if (h == handlers.end()) { return false; } /* Ok, so we found a handler that matched, so call open() */ if (!h->handler->open(path, std::ios::out)) { return false; } Session* curr; const char* active; if (path == hashBlobId) { /* 2c) are they opening the /flash/hash ? (to start the process) */ curr = &activeHash; active = activeHashBlobId; } else { curr = &activeImage; active = activeImageBlobId; } curr->flags = flags; curr->dataHandler = d->handler.get(); curr->imageHandler = h->handler.get(); lookup[session] = curr; addBlobId(active); removeBlobId(verifyBlobId); changeState(UpdateState::uploadInProgress); return true; } /** * The write command really just grabs the data from wherever it is and sends it * to the image handler. It's the image handler's responsibility to deal with * the data provided. * * This receives a session from the blob manager, therefore it is always called * between open() and close(). */ bool FirmwareBlobHandler::write(uint16_t session, uint32_t offset, const std::vector& data) { auto item = lookup.find(session); if (item == lookup.end()) { return false; } /* Prevent writing during verification. */ if (state == UpdateState::verificationStarted) { return false; } /* Prevent writing to the verification or update blobs. */ if (item->second->activePath == verifyBlobId || item->second->activePath == updateBlobId) { return false; } std::vector bytes; if (item->second->flags & FirmwareFlags::UpdateFlags::ipmi) { bytes = data; } else { /* little endian required per design, and so on, but TODO: do endianness * with boost. */ struct ExtChunkHdr header; if (data.size() != sizeof(header)) { return false; } std::memcpy(&header, data.data(), data.size()); bytes = item->second->dataHandler->copyFrom(header.length); } return item->second->imageHandler->write(offset, bytes); } /* * If the active session (image or hash) is over LPC, this allows * configuring it. This option is only available before you start * writing data for the given item (image or hash). This will return * false at any other part. -- the lpc handler portion will know to return * false. */ bool FirmwareBlobHandler::writeMeta(uint16_t session, uint32_t, const std::vector& data) { auto item = lookup.find(session); if (item == lookup.end()) { return false; } if (item->second->flags & FirmwareFlags::UpdateFlags::ipmi) { return false; } /* Prevent writing meta to the verification blob (it has no data handler). */ if (item->second->dataHandler) { return item->second->dataHandler->writeMeta(data); } return false; } /* * If this command is called on the session for the verifyBlobId, it'll * trigger a systemd service `verify_image.service` to attempt to verify * the image. * * For this file to have opened, the other two must be closed, which means any * out-of-band transport mechanism involved is closed. */ bool FirmwareBlobHandler::commit(uint16_t session, const std::vector&) { auto item = lookup.find(session); if (item == lookup.end()) { return false; } /* You can only commit on the verifyBlodId or updateBlobId */ if (item->second->activePath != verifyBlobId && item->second->activePath != updateBlobId) { std::fprintf(stderr, "path: '%s' not expected for commit\n", item->second->activePath.c_str()); return false; } switch (state) { case UpdateState::verificationPending: /* Set state to committing. */ item->second->flags |= blobs::StateFlags::committing; return triggerVerification(); case UpdateState::verificationStarted: /* Calling repeatedly has no effect within an update process. */ return true; case UpdateState::verificationCompleted: /* Calling after the verification process has completed returns * failure. */ return false; case UpdateState::updatePending: item->second->flags |= blobs::StateFlags::committing; return triggerUpdate(); case UpdateState::updateStarted: /* Calling repeatedly has no effect within an update process. */ return true; default: return false; } } /* * Close must be called on the firmware image before triggering * verification via commit. Once the verification is complete, you can * then close the hash file. * * If the `verify_image.service` returned success, closing the hash file * will have a specific behavior depending on the update. If it's UBI, * it'll perform the install. If it's static layout, it'll do nothing. The * verify_image service in the static layout case is responsible for placing * the file in the correct staging position. */ bool FirmwareBlobHandler::close(uint16_t session) { auto item = lookup.find(session); if (item == lookup.end()) { return false; } switch (state) { case UpdateState::uploadInProgress: /* They are closing a data pathway (image, tarball, hash). */ changeState(UpdateState::verificationPending); /* Add verify blob ID now that we can expect it, IF they also wrote * some data. */ if (std::count(blobIDs.begin(), blobIDs.end(), activeImageBlobId)) { addBlobId(verifyBlobId); } break; case UpdateState::verificationPending: /* They haven't triggered, therefore closing is uninteresting. */ break; case UpdateState::verificationStarted: /* Abort without checking to see if it happened to finish. Require * the caller to stat() deliberately. */ abortVerification(); abortProcess(); break; case UpdateState::verificationCompleted: if (lastVerificationStatus == ActionStatus::success) { changeState(UpdateState::updatePending); addBlobId(updateBlobId); removeBlobId(verifyBlobId); } else { /* Verification failed, and the host-tool knows this by calling * stat(), which triggered the state change to * verificationCompleted. * * Therefore, let's abort the process at this point. */ abortProcess(); } break; case UpdateState::updatePending: /* They haven't triggered the update, therefore this is * uninteresting. */ break; case UpdateState::updateStarted: /* Abort without checking to see if it happened to finish. Require * the caller to stat() deliberately. */ abortUpdate(); abortProcess(); break; case UpdateState::updateCompleted: if (lastUpdateStatus == ActionStatus::failed) { /* TODO: lOG something? */ std::fprintf(stderr, "Update failed\n"); } abortProcess(); break; default: break; } if (!lookup.empty()) { if (item->second->dataHandler) { item->second->dataHandler->close(); } if (item->second->imageHandler) { item->second->imageHandler->close(); } lookup.erase(item); } return true; } void FirmwareBlobHandler::changeState(UpdateState next) { state = next; if (state == UpdateState::notYetStarted) { /* Going back to notyetstarted, let them trigger preparation again. */ preparationTriggered = false; } else if (state == UpdateState::uploadInProgress) { /* Store this transition logic here instead of ::open() */ if (!preparationTriggered) { auto* pack = getActionPack(); if (pack) { pack->preparation->trigger(); preparationTriggered = true; } } } } bool FirmwareBlobHandler::expire(uint16_t) { abortProcess(); return true; } /* * Currently, the design does not provide this with a function, however, * it will likely change to support reading data back. */ std::vector FirmwareBlobHandler::read(uint16_t, uint32_t, uint32_t) { return {}; } void FirmwareBlobHandler::abortProcess() { /* Closing of open files is handled from close() -- Reaching here from * delete may never be supported. */ removeBlobId(verifyBlobId); removeBlobId(updateBlobId); removeBlobId(activeImageBlobId); removeBlobId(activeHashBlobId); for (auto item : lookup) { if (item.second->dataHandler) { item.second->dataHandler->close(); } if (item.second->imageHandler) { item.second->imageHandler->close(); } } lookup.clear(); openedFirmwareType = ""; changeState(UpdateState::notYetStarted); } void FirmwareBlobHandler::abortVerification() { auto* pack = getActionPack(); if (pack) { pack->verification->abort(); } } bool FirmwareBlobHandler::triggerVerification() { auto* pack = getActionPack(); if (!pack) { return false; } bool result = pack->verification->trigger(); if (result) { changeState(UpdateState::verificationStarted); } return result; } void FirmwareBlobHandler::abortUpdate() { auto* pack = getActionPack(); if (pack) { pack->update->abort(); } } bool FirmwareBlobHandler::triggerUpdate() { auto* pack = getActionPack(); if (!pack) { return false; } bool result = pack->update->trigger(); if (result) { changeState(UpdateState::updateStarted); } return result; } } // namespace ipmi_flash