blob: f13d039c81cb2bba16ab3838ed59f86d89deb040 [file] [log] [blame]
// 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