| // Copyright 2024 Google LLC |
| // |
| // 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 "payload_update.hpp" |
| |
| #include "google3/host_commands.h" |
| |
| #include "message_util.hpp" |
| #include "sys.hpp" |
| |
| #include <fcntl.h> |
| #include <unistd.h> |
| |
| #include <boost/endian/conversion.hpp> |
| #include <stdplus/print.hpp> |
| #include <stdplus/raw.hpp> |
| #include <xyz/openbmc_project/Control/Hoth/error.hpp> |
| |
| #include <array> |
| #include <cstdint> |
| #include <format> |
| #include <memory> |
| #include <span> |
| #include <string> |
| #include <vector> |
| |
| namespace google |
| { |
| namespace hoth |
| { |
| namespace internal |
| { |
| |
| enum hoth_status : uint16_t |
| { |
| /* |
| There is no update payload pending confirmation |
| */ |
| HOTH_PAYLOAD_UPDATE_CONFIRM_NO_PENDING_PAYLOAD = 0xD004, |
| }; |
| |
| using sdbusplus::error::xyz::openbmc_project::control::hoth::ResponseFailure; |
| |
| void PayloadUpdateImpl::initiate() const |
| { |
| sendCommand(PAYLOAD_UPDATE_INITIATE); |
| } |
| |
| void PayloadUpdateImpl::erase(const uint32_t offset, const uint32_t size) const |
| { |
| sendCommand(PAYLOAD_UPDATE_ERASE, offset, size); |
| } |
| |
| bool PayloadUpdateImpl::findDescriptor(const std::string& path, |
| uint32_t* desc_offset) const |
| { |
| |
| Fd fd(sys->open(path.c_str(), O_RDONLY), sys); |
| |
| if (*fd < 0) |
| { |
| (void)fd.release(); |
| throw errnoException(std::format("Failed to open file {}", path)); |
| } |
| |
| auto seekRet = sys->lseek(*fd, 0, SEEK_END); |
| size_t fsize = seekRet; |
| |
| if (seekRet < 0) |
| { |
| throw errnoException(std::format("Error seeking on file", path)); |
| } |
| |
| uint64_t read_data = 0; |
| bool desc_found = false; |
| |
| // Find the nearest aligned address to the end of the file. We can't |
| // assume that the file-size is aligned. |
| ssize_t offset = |
| static_cast<ssize_t>((fsize / kImageDescriptorAlignment) * kImageDescriptorAlignment); |
| if (offset + sizeof(kDescriptorMagic) > fsize) |
| { |
| offset -= kImageDescriptorAlignment; |
| } |
| |
| for (; offset >= 0; offset -= kImageDescriptorAlignment) |
| { |
| auto seek = sys->lseek(*fd, offset, SEEK_SET); |
| if (seek < 0) |
| { |
| throw errnoException(std::format("Error seeking on file", path)); |
| } |
| |
| auto actualReadSize = sys->read(*fd, &read_data, sizeof(read_data)); |
| |
| if (actualReadSize < 0) |
| { |
| throw errnoException( |
| std::format("Failed to read from file {}", path)); |
| } |
| |
| if (read_data == kDescriptorMagic) |
| { |
| if (desc_offset) |
| { |
| *desc_offset = offset; |
| desc_found = true; |
| break; |
| } |
| } |
| } |
| |
| return desc_found; |
| } |
| |
| void PayloadUpdateImpl::eraseAndSendStaticWPRegions(const std::string& path, |
| uint32_t desc_offset) const |
| { |
| |
| Fd fd(sys->open(path.c_str(), O_RDONLY), sys); |
| |
| if (*fd < 0) |
| { |
| (void)fd.release(); |
| throw errnoException(std::format("Failed to open file {}", path)); |
| } |
| |
| struct image_descriptor descriptor; |
| |
| if (sys->lseek(*fd, desc_offset, SEEK_SET) < 0) |
| { |
| throw errnoException(std::format("Error seeking on file", path)); |
| } |
| auto actualReadSize = sys->read(*fd, &descriptor, sizeof(descriptor)); |
| |
| if (actualReadSize < 0) |
| { |
| throw errnoException(std::format("Failed to read from file {}", path)); |
| } |
| |
| if (descriptor.descriptor_magic != DESCRIPTOR_MAGIC || |
| descriptor.descriptor_offset != desc_offset || |
| descriptor.region_count == 0) |
| { |
| throw errnoException( |
| std::format("Invalid descriptor at offset {}", path)); |
| } |
| |
| std::vector<struct image_region> image_regions(descriptor.region_count); |
| |
| actualReadSize = |
| sys->read(*fd, image_regions.data(), |
| sizeof(struct image_region) * descriptor.region_count); |
| if (actualReadSize < 0) |
| { |
| throw errnoException(std::format("Failed to read from file {}", path)); |
| } |
| |
| // Erase all the static and Write Protected regions of the payload |
| for (uint32_t i = 0; i < descriptor.region_count; i++) |
| { |
| if ((image_regions[i].region_attributes & IMAGE_REGION_STATIC) || |
| (image_regions[i].region_attributes & IMAGE_REGION_WRITE_PROTECTED)) |
| { |
| if (image_regions[i].region_size == 0 || |
| (image_regions[i].region_size % kSectorSizeBytes) != 0) |
| { |
| throw errnoException(std::format("invalid staging area size")); |
| } |
| uint32_t regionOffset = image_regions[i].region_offset; |
| uint32_t toEraseSize = image_regions[i].region_size; |
| while (toEraseSize >= kEraseChunkSizeBytes) |
| { |
| sendCommand(PAYLOAD_UPDATE_ERASE, regionOffset, |
| kEraseChunkSizeBytes); |
| regionOffset += kEraseChunkSizeBytes; |
| toEraseSize -= kEraseChunkSizeBytes; |
| } |
| if (toEraseSize > 0) |
| { |
| sendCommand(PAYLOAD_UPDATE_ERASE, regionOffset, toEraseSize); |
| } |
| } |
| } |
| |
| // Read and send static and WP sections of the payload |
| for (uint32_t i = 0; i < descriptor.region_count; i++) |
| { |
| if ((image_regions[i].region_attributes & IMAGE_REGION_STATIC) || |
| (image_regions[i].region_attributes & IMAGE_REGION_WRITE_PROTECTED)) |
| { |
| uint32_t offset = image_regions[i].region_offset; |
| auto regionSize = image_regions[i].region_size; |
| size_t readSize = 0; |
| std::vector<uint8_t> readBuffer(max_packet_size); |
| |
| if (sys->lseek(*fd, offset, SEEK_SET) != offset) |
| { |
| throw errnoException( |
| std::format("Failed to read file size {}", path)); |
| } |
| do |
| { |
| if (regionSize <= max_packet_size) |
| { |
| readSize = regionSize; |
| } else |
| { |
| readSize = max_packet_size; |
| } |
| |
| if (!sys->read(*fd, readBuffer.data(), readSize)) |
| { |
| throw errnoException( |
| std::format("Failed to read from file {}", path)); |
| } |
| |
| // Decided not to use initializer list to create the span here |
| // as actualReadSize is ssize_t and we'd need to static_cast |
| trimAndSend(std::span<uint8_t>(readBuffer.data(), readSize), |
| offset); |
| offset += readSize; |
| regionSize -= readSize; |
| |
| } while (regionSize); // actualReadSize will be 0 when EOF is |
| // reached, exit loop |
| } |
| } |
| } |
| |
| void PayloadUpdateImpl::eraseAndSendStaticWP(const std::string& path) const |
| { |
| |
| uint32_t desc_offset = 0; |
| |
| if (!findDescriptor(path, &desc_offset)) |
| { |
| throw errnoException(std::format("No valid descriptor found")); |
| } |
| |
| eraseAndSendStaticWPRegions(path, desc_offset); |
| } |
| |
| void PayloadUpdateImpl::read(uint32_t offset, std::span<uint8_t> data) const |
| { |
| std::vector<uint8_t> buf; |
| auto rsp = sendCommand(buf, PAYLOAD_UPDATE_READ, offset, data.size()); |
| if (rsp.size() != data.size()) |
| { |
| stdplus::print( |
| stderr, |
| "Payload command received a short response from Hoth `{} < {}`\n", |
| rsp.size(), data.size()); |
| throw ResponseFailure(); |
| } |
| memcpy(data.data(), rsp.data(), data.size()); |
| } |
| |
| void PayloadUpdateImpl::verify() const |
| { |
| payload_update_packet request = {}; |
| request.type = PAYLOAD_UPDATE_VERIFY_DESCRIPTOR; |
| auto buf = hostCmd->sendCommand(EC_CMD_BOARD_SPECIFIC_BASE + |
| EC_PRV_CMD_HOTH_PAYLOAD_UPDATE, |
| 0, &request, sizeof(request)); |
| std::span<uint8_t> rsp = buf; |
| auto& hdr = stdplus::raw::extractRef<RspHeader>(rsp); |
| |
| // In case PAYLOAD_UPDATE_VERIFY is not supported (EC_RES_ERROR), |
| // we fall back to PAYLOAD_UPDATE_VERIFY instead. |
| if (hdr.result == EC_RES_INVALID_PARAM || hdr.result == EC_RES_ERROR) |
| { |
| sendCommand(PAYLOAD_UPDATE_VERIFY); |
| return; |
| } |
| if (hdr.result != EC_RES_SUCCESS) |
| { |
| stdplus::print(stderr, "VERIFY_DESCRIPTOR error: {:#x}\n", |
| static_cast<uint8_t>(hdr.result)); |
| throw ResponseFailure(); |
| } |
| } |
| |
| payload_update_status PayloadUpdateImpl::getStatus() const |
| { |
| std::vector<uint8_t> buf; |
| std::span<const uint8_t> output = |
| sendCommand(buf, PAYLOAD_UPDATE_GET_STATUS); |
| |
| return stdplus::raw::copyFrom<payload_update_status>(output); |
| } |
| |
| void PayloadUpdateImpl::activate(Side side, Persistence persistence) const |
| { |
| ActivateRequest request; |
| request.header.offset = 0; |
| request.header.len = sizeof(request.activate); |
| request.header.type = PAYLOAD_UPDATE_ACTIVATE; |
| request.activate.half = static_cast<uint8_t>(side); |
| request.activate.make_persistent = static_cast<uint8_t>(persistence); |
| |
| sendCommand( |
| {reinterpret_cast<const uint8_t*>(&request), sizeof(ActivateRequest)}); |
| } |
| |
| std::optional<payload_update_confirm_response> |
| PayloadUpdateImpl::confirm(enum payload_update_confirm_option option, |
| uint32_t timeout, |
| uint64_t confirmation_cookie) const |
| { |
| |
| ConfirmRequest request; |
| request.header.offset = 0; |
| request.header.len = sizeof(request.confirm); |
| request.header.type = PAYLOAD_UPDATE_CONFIRM; |
| request.confirm.option = option; |
| request.confirm.timeout_value = boost::endian::native_to_little(timeout); |
| request.confirm.confirmation_cookie = |
| boost::endian::native_to_little(confirmation_cookie); |
| |
| std::vector<uint8_t> buf = hostCmd->sendCommand( |
| EC_CMD_BOARD_SPECIFIC_BASE + EC_PRV_CMD_HOTH_PAYLOAD_UPDATE, 0, |
| &request, sizeof(request)); |
| // Extract the response header out when returning the span |
| std::span<const uint8_t> output = buf; |
| auto rsp = stdplus::raw::extract<RspHeader>(output); |
| if (rsp.result == HOTH_PAYLOAD_UPDATE_CONFIRM_NO_PENDING_PAYLOAD) |
| { |
| return std::nullopt; |
| } |
| |
| if (rsp.result != EC_RES_SUCCESS) |
| { |
| stdplus::print( |
| stderr, |
| "Payload confirm command received a bad response from Hoth {:#x}\n", |
| static_cast<uint8_t>(rsp.result)); |
| throw ResponseFailure(); |
| } |
| |
| return stdplus::raw::copyFrom<payload_update_confirm_response>(output); |
| } |
| |
| namespace |
| { |
| // Helper functions for PayloadUpdateImpl::send |
| size_t SizeToSkip(std::span<const uint8_t> data) |
| { |
| size_t i; |
| for (i = 0; i < data.size(); i++) |
| { |
| if (data[i] != 0xff) |
| { |
| break; |
| } |
| } |
| return i; |
| } |
| |
| size_t SizeToSend(std::span<const uint8_t> data) |
| { |
| size_t i; |
| for (i = data.size(); i > 0; i--) |
| { |
| if (data[i - 1] != 0xff) |
| { |
| break; |
| } |
| } |
| return i; |
| } |
| } // namespace |
| |
| void PayloadUpdateImpl::send(const std::string& path) const |
| { |
| Fd fd(sys->open(path.c_str(), O_RDONLY), sys); |
| |
| if (*fd < 0) |
| { |
| (void)fd.release(); |
| throw errnoException(std::format("Failed to open file {}", path)); |
| } |
| |
| std::array<uint8_t, max_packet_size> readBuffer; |
| uint32_t offset = 0; |
| ssize_t actualReadSize = 0; |
| // Read and send payload until we reach EOF of the given file |
| do |
| { |
| actualReadSize = sys->read(*fd, readBuffer.data(), max_packet_size); |
| |
| if (actualReadSize < 0) |
| { |
| throw errnoException( |
| std::format("Failed to read from file {}", path)); |
| } |
| if (actualReadSize > 0) { |
| // Decided not to use initializer list to create the span here as |
| // actualReadSize is ssize_t and we'd need to static_cast |
| trimAndSend(std::span<uint8_t>(readBuffer.data(), actualReadSize), |
| offset); |
| offset += actualReadSize; |
| } |
| // actualReadSize will be 0 when EOF is reached, exit loop |
| } while (actualReadSize); |
| } |
| |
| /* Parse and send the given data after trimming 0xff from the front and back |
| * - This works because PayloadUpdateImpl::Initiate erased the |
| * EEPROM to 0xff. |
| * - This is also more efficient than sending everything, as our |
| * image binaries tend to have large sections of 0xff and we can |
| * avoid sending those sections. |
| */ |
| void PayloadUpdateImpl::trimAndSend(std::span<const uint8_t> data, |
| uint32_t globalOffset) const |
| { |
| // Allocate the payload buffer at "buffer". |
| // "request" will point to the payload_update_packet section of the buffer |
| // "request_payload" will point to the beginning of the payload |
| // (after the payload_update_packet header). |
| std::array<uint8_t, sizeof(payload_update_packet) + max_packet_size> buffer; |
| payload_update_packet* request = |
| reinterpret_cast<payload_update_packet*>(buffer.data()); |
| uint8_t* request_payload = &buffer[sizeof(payload_update_packet)]; |
| |
| const size_t dataSize = data.size(); |
| if (dataSize > max_packet_size) |
| { |
| throw errnoException( |
| "data to send cannot be bigger than max_packet_size"); |
| } |
| size_t localOffset = SizeToSkip(data); |
| if (localOffset >= dataSize) |
| { |
| // No need to send any payload |
| return; |
| } |
| size_t max_size_to_send = std::min(dataSize - localOffset, max_packet_size); |
| size_t size_to_send = |
| SizeToSend(data.subspan(localOffset, max_size_to_send)); |
| |
| // Construct and send payload |
| request->offset = globalOffset + localOffset; |
| request->len = size_to_send; |
| request->type = PAYLOAD_UPDATE_CONTINUE; |
| memcpy(request_payload, &data[localOffset], size_to_send); |
| sendCommand({buffer.data(), sizeof(*request) + size_to_send}); |
| } |
| |
| void PayloadUpdateImpl::sendCommand(uint8_t command, uint32_t offset, |
| uint32_t len) const |
| { |
| std::vector<uint8_t> buf; |
| (void)sendCommand(buf, command, offset, len); |
| } |
| |
| void PayloadUpdateImpl::sendCommand(std::span<const uint8_t> request) const |
| { |
| std::vector<uint8_t> buf; |
| (void)sendCommand(buf, request); |
| } |
| |
| [[nodiscard]] std::span<const uint8_t> |
| PayloadUpdateImpl::sendCommand(std::vector<uint8_t>& buf, uint8_t command, |
| uint32_t offset, uint32_t len) const |
| { |
| // For most basic commands, request offset and length are zero |
| payload_update_packet request; |
| request.offset = offset; |
| request.len = len; |
| request.type = command; |
| return sendCommand(buf, {reinterpret_cast<const uint8_t*>(&request), |
| sizeof(payload_update_packet)}); |
| } |
| |
| [[nodiscard]] std::span<const uint8_t> |
| PayloadUpdateImpl::sendCommand(std::vector<uint8_t>& buf, |
| std::span<const uint8_t> request) const |
| { |
| buf = hostCmd->sendCommand(EC_CMD_BOARD_SPECIFIC_BASE + |
| EC_PRV_CMD_HOTH_PAYLOAD_UPDATE, |
| 0, request.data(), request.size()); |
| // Extract the response header out when returning the span |
| std::span<const uint8_t> output = buf; |
| auto rsp = stdplus::raw::extract<RspHeader>(output); |
| if (rsp.result != EC_RES_SUCCESS) |
| { |
| stdplus::print( |
| stderr, "Payload command received a bad response from Hoth {:#x}\n", |
| static_cast<uint8_t>(rsp.result)); |
| throw ResponseFailure(); |
| } |
| |
| return output; |
| } |
| |
| } // namespace internal |
| |
| } // namespace hoth |
| |
| } // namespace google |