| #include "config.h" |
| |
| #include "buffer.hpp" |
| |
| #include "pci_handler.hpp" |
| |
| #include <boost/endian/arithmetic.hpp> |
| #include <boost/endian/conversion.hpp> |
| |
| #include <algorithm> |
| #include <array> |
| #include <cstddef> |
| #include <cstdint> |
| #include <format> |
| #include <memory> |
| #include <numeric> |
| #include <span> |
| #include <vector> |
| |
| namespace bios_bmc_smm_error_logger |
| { |
| |
| BufferImpl::BufferImpl(std::unique_ptr<DataInterface> dataInterface) : |
| dataInterface(std::move(dataInterface)) {}; |
| |
| void BufferImpl::initialize(uint32_t bmcInterfaceVersion, uint16_t queueSize, |
| uint16_t ueRegionSize, |
| const std::array<uint32_t, 4>& magicNumber) |
| { |
| const size_t memoryRegionSize = dataInterface->getMemoryRegionSize(); |
| if (queueSize > memoryRegionSize) |
| { |
| throw std::runtime_error(std::format( |
| "[initialize] Proposed queue size '{}' is bigger than the " |
| "BMC's allocated MMIO region of '{}'", |
| queueSize, memoryRegionSize)); |
| } |
| |
| // Initialize the whole buffer with 0x00 |
| const std::vector<uint8_t> emptyVector(queueSize, 0); |
| size_t byteWritten = dataInterface->write(0, emptyVector); |
| if (byteWritten != queueSize) |
| { |
| throw std::runtime_error( |
| std::format("[initialize] Only erased '{}'", byteWritten)); |
| } |
| |
| // Create an initial buffer header and write to it |
| struct CircularBufferHeader initializationHeader = {}; |
| initializationHeader.bmcInterfaceVersion = |
| boost::endian::native_to_little(bmcInterfaceVersion); |
| initializationHeader.queueSize = boost::endian::native_to_little(queueSize); |
| initializationHeader.ueRegionSize = |
| boost::endian::native_to_little(ueRegionSize); |
| std::transform(magicNumber.begin(), magicNumber.end(), |
| initializationHeader.magicNumber.begin(), |
| [](uint32_t number) -> little_uint32_t { |
| return boost::endian::native_to_little(number); |
| }); |
| |
| uint8_t* initializationHeaderPtr = |
| reinterpret_cast<uint8_t*>(&initializationHeader); |
| size_t initializationHeaderSize = sizeof(initializationHeader); |
| byteWritten = dataInterface->write( |
| 0, std::span<const uint8_t>( |
| initializationHeaderPtr, |
| initializationHeaderPtr + initializationHeaderSize)); |
| if (byteWritten != initializationHeaderSize) |
| { |
| throw std::runtime_error(std::format( |
| "[initialize] Only wrote '{}' bytes of the header", byteWritten)); |
| } |
| cachedBufferHeader = initializationHeader; |
| } |
| |
| void BufferImpl::readBufferHeader() |
| { |
| size_t headerSize = sizeof(struct CircularBufferHeader); |
| std::vector<uint8_t> bytesRead = |
| dataInterface->read(/*offset=*/0, headerSize); |
| |
| if (bytesRead.size() != headerSize) |
| { |
| throw std::runtime_error( |
| std::format("Buffer header read only read '{}', expected '{}'", |
| bytesRead.size(), headerSize)); |
| } |
| |
| cachedBufferHeader = |
| *reinterpret_cast<struct CircularBufferHeader*>(bytesRead.data()); |
| }; |
| |
| struct CircularBufferHeader BufferImpl::getCachedBufferHeader() const |
| { |
| return cachedBufferHeader; |
| } |
| |
| void BufferImpl::updateReadPtr(const uint32_t newReadPtr) |
| { |
| constexpr uint8_t bmcReadPtrOffset = |
| offsetof(struct CircularBufferHeader, bmcReadPtr); |
| |
| little_uint24_t truncatedReadPtr = |
| boost::endian::native_to_little(newReadPtr & 0xffffff); |
| uint8_t* truncatedReadPtrPtr = |
| reinterpret_cast<uint8_t*>(&truncatedReadPtr); |
| |
| size_t writtenSize = dataInterface->write( |
| bmcReadPtrOffset, std::span<const uint8_t>{ |
| truncatedReadPtrPtr, |
| truncatedReadPtrPtr + sizeof(truncatedReadPtr)}); |
| if (writtenSize != sizeof(truncatedReadPtr)) |
| { |
| throw std::runtime_error(std::format( |
| "[updateReadPtr] Wrote '{}' bytes, instead of expected '{}'", |
| writtenSize, sizeof(truncatedReadPtr))); |
| } |
| cachedBufferHeader.bmcReadPtr = truncatedReadPtr; |
| } |
| |
| void BufferImpl::updateBmcFlags(const uint32_t newBmcFlag) |
| { |
| constexpr uint8_t bmcFlagsPtrOffset = |
| offsetof(struct CircularBufferHeader, bmcFlags); |
| |
| little_uint32_t littleNewBmcFlag = |
| boost::endian::native_to_little(newBmcFlag); |
| uint8_t* littleNewBmcFlagPtr = |
| reinterpret_cast<uint8_t*>(&littleNewBmcFlag); |
| |
| size_t writtenSize = dataInterface->write( |
| bmcFlagsPtrOffset, std::span<const uint8_t>{ |
| littleNewBmcFlagPtr, |
| littleNewBmcFlagPtr + sizeof(little_uint32_t)}); |
| if (writtenSize != sizeof(little_uint32_t)) |
| { |
| throw std::runtime_error(std::format( |
| "[updateBmcFlags] Wrote '{}' bytes, instead of expected '{}'", |
| writtenSize, sizeof(little_uint32_t))); |
| } |
| cachedBufferHeader.bmcFlags = littleNewBmcFlag; |
| } |
| |
| std::vector<uint8_t> BufferImpl::wraparoundRead(const uint32_t relativeOffset, |
| const uint32_t length) |
| { |
| const size_t maxOffset = getMaxOffset(); |
| |
| if (relativeOffset > maxOffset) |
| { |
| throw std::runtime_error( |
| std::format("[wraparoundRead] relativeOffset '{}' was bigger " |
| "than maxOffset '{}'", |
| relativeOffset, maxOffset)); |
| } |
| if (length > maxOffset) |
| { |
| throw std::runtime_error(std::format( |
| "[wraparoundRead] length '{}' was bigger than maxOffset '{}'", |
| length, maxOffset)); |
| } |
| |
| // Do a calculation to see if the read will wraparound |
| const size_t queueOffset = getQueueOffset(); |
| const size_t writableSpace = maxOffset - relativeOffset; |
| size_t numWraparoundBytesToRead = 0; |
| if (length > writableSpace) |
| { |
| // This means we will wrap, count the bytes that are left to read |
| numWraparoundBytesToRead = length - writableSpace; |
| } |
| const size_t numBytesToReadTillQueueEnd = length - numWraparoundBytesToRead; |
| |
| std::vector<uint8_t> bytesRead = dataInterface->read( |
| queueOffset + relativeOffset, numBytesToReadTillQueueEnd); |
| if (bytesRead.size() != numBytesToReadTillQueueEnd) |
| { |
| throw std::runtime_error( |
| std::format("[wraparoundRead] Read '{}' which was not " |
| "the requested length of '{}'", |
| bytesRead.size(), numBytesToReadTillQueueEnd)); |
| } |
| size_t updatedReadPtr = relativeOffset + numBytesToReadTillQueueEnd; |
| if (updatedReadPtr == maxOffset) |
| { |
| // If we read all the way up to the end of the queue, we need to |
| // manually wrap the updateReadPtr around to 0 |
| updatedReadPtr = 0; |
| } |
| |
| // If there are any more bytes to be read beyond the buffer, wrap around and |
| // read from the beginning of the buffer (offset by the queueOffset) |
| if (numWraparoundBytesToRead > 0) |
| { |
| std::vector<uint8_t> wrappedBytesRead = |
| dataInterface->read(queueOffset, numWraparoundBytesToRead); |
| if (numWraparoundBytesToRead != wrappedBytesRead.size()) |
| { |
| throw std::runtime_error(std::format( |
| "[wraparoundRead] Buffer wrapped around but read '{}' which " |
| "was not the requested lenght of '{}'", |
| wrappedBytesRead.size(), numWraparoundBytesToRead)); |
| } |
| bytesRead.insert(bytesRead.end(), wrappedBytesRead.begin(), |
| wrappedBytesRead.end()); |
| updatedReadPtr = numWraparoundBytesToRead; |
| } |
| updateReadPtr(updatedReadPtr); |
| |
| return bytesRead; |
| } |
| |
| struct QueueEntryHeader BufferImpl::readEntryHeader() |
| { |
| size_t headerSize = sizeof(struct QueueEntryHeader); |
| // wraparonudRead will throw if it did not read all the bytes, let it |
| // propagate up the stack |
| std::vector<uint8_t> bytesRead = wraparoundRead( |
| boost::endian::little_to_native(cachedBufferHeader.bmcReadPtr), |
| headerSize); |
| |
| return *reinterpret_cast<struct QueueEntryHeader*>(bytesRead.data()); |
| } |
| |
| EntryPair BufferImpl::readEntry() |
| { |
| struct QueueEntryHeader entryHeader = readEntryHeader(); |
| size_t entrySize = boost::endian::little_to_native(entryHeader.entrySize); |
| |
| // wraparonudRead may throw if entrySize was bigger than the buffer or if it |
| // was not able to read all the bytes, let it propagate up the stack |
| std::vector<uint8_t> entry = wraparoundRead( |
| boost::endian::little_to_native(cachedBufferHeader.bmcReadPtr), |
| entrySize); |
| |
| // Calculate the checksum |
| uint8_t* entryHeaderPtr = reinterpret_cast<uint8_t*>(&entryHeader); |
| uint8_t checksum = |
| std::accumulate(entryHeaderPtr, |
| entryHeaderPtr + sizeof(struct QueueEntryHeader), 0, |
| std::bit_xor<void>()) ^ |
| std::accumulate(entry.begin(), entry.end(), 0, std::bit_xor<void>()); |
| |
| if (checksum != 0) |
| { |
| throw std::runtime_error(std::format( |
| "[readEntry] Checksum was '{}', expected '0'", checksum)); |
| } |
| |
| return {entryHeader, entry}; |
| } |
| |
| std::vector<EntryPair> BufferImpl::readErrorLogs() |
| { |
| // Reading the buffer header will update the cachedBufferHeader |
| readBufferHeader(); |
| |
| const size_t maxOffset = getMaxOffset(); |
| size_t currentBiosWritePtr = |
| boost::endian::little_to_native(cachedBufferHeader.biosWritePtr); |
| if (currentBiosWritePtr > maxOffset) |
| { |
| throw std::runtime_error(std::format( |
| "[readErrorLogs] currentBiosWritePtr was '{}' which was bigger " |
| "than maxOffset '{}'", |
| currentBiosWritePtr, maxOffset)); |
| } |
| size_t currentReadPtr = |
| boost::endian::little_to_native(cachedBufferHeader.bmcReadPtr); |
| if (currentReadPtr > maxOffset) |
| { |
| throw std::runtime_error(std::format( |
| "[readErrorLogs] currentReadPtr was '{}' which was bigger " |
| "than maxOffset '{}'", |
| currentReadPtr, maxOffset)); |
| } |
| |
| size_t bytesToRead; |
| if (currentBiosWritePtr == currentReadPtr) |
| { |
| // No new payload was detected, return an empty vector gracefully |
| return {}; |
| } |
| |
| if (currentBiosWritePtr > currentReadPtr) |
| { |
| // Simply subtract in this case |
| bytesToRead = currentBiosWritePtr - currentReadPtr; |
| } |
| else |
| { |
| // Calculate the bytes to the "end" (maxOffset - ReadPtr) + |
| // bytes to read from the "beginning" (0 + WritePtr) |
| bytesToRead = (maxOffset - currentReadPtr) + currentBiosWritePtr; |
| } |
| |
| size_t byteRead = 0; |
| std::vector<EntryPair> entryPairs; |
| while (byteRead < bytesToRead) |
| { |
| EntryPair entryPair = readEntry(); |
| byteRead += sizeof(struct QueueEntryHeader) + entryPair.second.size(); |
| entryPairs.push_back(entryPair); |
| |
| // Note: readEntry() will update cachedBufferHeader.bmcReadPtr |
| currentReadPtr = |
| boost::endian::little_to_native(cachedBufferHeader.bmcReadPtr); |
| } |
| if (currentBiosWritePtr != currentReadPtr) |
| { |
| throw std::runtime_error(std::format( |
| "[readErrorLogs] biosWritePtr '{}' and bmcReaddPtr '{}' " |
| "are not identical after reading through all the logs", |
| currentBiosWritePtr, currentReadPtr)); |
| } |
| return entryPairs; |
| } |
| |
| size_t BufferImpl::getMaxOffset() |
| { |
| size_t queueSize = |
| boost::endian::little_to_native(cachedBufferHeader.queueSize); |
| size_t ueRegionSize = |
| boost::endian::little_to_native(cachedBufferHeader.ueRegionSize); |
| |
| if (queueSize != QUEUE_REGION_SIZE) |
| { |
| throw std::runtime_error(std::format( |
| "[{}] runtime queueSize '{}' did not match compile-time queueSize " |
| "'{}'. This indicates that the buffer was corrupted", |
| __FUNCTION__, queueSize, QUEUE_REGION_SIZE)); |
| } |
| if (ueRegionSize != UE_REGION_SIZE) |
| { |
| throw std::runtime_error(std::format( |
| "[{}] runtime ueRegionSize '{}' did not match compile-time " |
| "ueRegionSize '{}'. This indicates that the buffer was corrupted", |
| __FUNCTION__, ueRegionSize, UE_REGION_SIZE)); |
| } |
| |
| return queueSize - ueRegionSize - sizeof(struct CircularBufferHeader); |
| } |
| |
| size_t BufferImpl::getQueueOffset() |
| { |
| size_t ueRegionSize = |
| boost::endian::little_to_native(cachedBufferHeader.ueRegionSize); |
| |
| if (ueRegionSize != UE_REGION_SIZE) |
| { |
| throw std::runtime_error(std::format( |
| "[{}] runtime ueRegionSize '{}' did not match compile-time " |
| "ueRegionSize '{}'. This indicates that the buffer was corrupted", |
| __FUNCTION__, ueRegionSize, UE_REGION_SIZE)); |
| } |
| return sizeof(struct CircularBufferHeader) + ueRegionSize; |
| } |
| |
| } // namespace bios_bmc_smm_error_logger |