blob: a454b2c7313fd2a1270d9c5bf35e29a3c41e92f8 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "fastboot.h"
#include <lib/efi/testing/fake_disk_io_protocol.h>
#include <algorithm>
#include <queue>
#include <set>
#include <string>
#include <string_view>
#include <vector>
#include <efi/system-table.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "diskio_test.h"
#include "tcp_test.h"
#include "util.h"
#include "xefi.h"
namespace gigaboot {
namespace {
using ::testing::Contains;
using ::testing::Return;
constexpr uint16_t kTestUdpPort = 1234;
constexpr ip6_addr kTestIpAddress = {0x01, 0x23, 0x45, 0x67};
// Defined by fastboot documentation.
enum UdpPacketType {
kUdpPacketTypeError = 0x00,
kUdpPacketTypeQuery = 0x01,
kUdpPacketTypeInit = 0x02,
kUdpPacketTypeFastboot = 0x03
};
// Extracts a network-order uint16 from the given data.
uint16_t ExtractU16(const uint8_t* data) { return static_cast<uint16_t>(data[0] << 8) | data[1]; }
// Extracts a uint16 MSB and LSB to reduce boilerplate.
uint8_t U16Msb(uint16_t value) { return value >> 8; }
uint8_t U16Lsb(uint16_t value) { return value & 0xFF; }
// Returns true if |data| starts with |prefix|.
bool StartsWith(const std::vector<uint8_t>& data, const std::vector<uint8_t>& prefix) {
return data.size() >= prefix.size() && memcmp(data.data(), prefix.data(), prefix.size()) == 0;
}
// Returns a 8-byte network-order TCP packet size as a string.
std::string TcpSizeString(uint64_t size) {
const uint64_t network_order_size = htonll(size);
std::string ret(sizeof(network_order_size), '\0');
memcpy(&ret[0], &network_order_size, sizeof(network_order_size));
return ret;
}
// Returns a fastboot TCP packet as a string.
// TCP packets consist of 8 bytes length + contents.
std::string TcpPacket(std::string_view contents) {
return TcpSizeString(contents.length()) + std::string(contents);
}
// Helper to fake fastboot UDP input and output.
//
// On creation, injects custom UDP functions into fastboot so we can run the
// loop while controlling what gets sent. On deletion, returns fastboot to
// normal functionality.
//
// Only one of these object can live at a time.
class FakeFastbootUdp {
public:
FakeFastbootUdp() {
if (instance_) {
fprintf(stderr, "ERROR: cannot create multiple FakeFastbootUdp objects - exiting\n");
exit(1);
}
instance_ = this;
fb_set_udp_functions_for_testing(RawPollFunc, RawSendFunc);
}
~FakeFastbootUdp() {
instance_ = nullptr;
fb_set_udp_functions_for_testing(nullptr, nullptr);
}
// Queues a packet to be sent into fastboot on the next loop.
void QueuePacket(std::vector<uint8_t> data) { tx_packets_.push(std::move(data)); }
// Pops the earliest packet received from fastboot, or an empty packet if none
// exist.
std::vector<uint8_t> ReceivePacket() {
if (rx_packets_.empty()) {
return {};
}
auto ret = std::move(rx_packets_.front());
rx_packets_.pop();
return ret;
}
// Sends and receives the 4 packets necessary to initialize a fastboot UDP
// session: TX query -> RX query response -> TX init -> RX init response.
//
// Registers test failure on error.
void InitializeSession() {
fb_bootimg_t bootimg;
// Send a query packet to get the current sequence number.
const auto query_packet = std::vector<uint8_t>{kUdpPacketTypeQuery, 0, 0, 0};
QueuePacket(query_packet);
ASSERT_EQ(POLL, fb_poll(&bootimg));
// Response packet should be the same query with 2 additional bytes giving
// the current sequence number.
auto response = ReceivePacket();
ASSERT_EQ(response.size(), 6u);
ASSERT_TRUE(StartsWith(response, query_packet));
uint16_t sequence = ExtractU16(&response[4]);
// Send an init packet: version = 0x0001, max packet size = 0x0800.
QueuePacket(
{kUdpPacketTypeInit, 0x00, U16Msb(sequence), U16Lsb(sequence), 0x00, 0x01, 0x08, 0x00});
ASSERT_EQ(POLL, fb_poll(&bootimg));
response = ReceivePacket();
ASSERT_EQ(response.size(), 8u);
ASSERT_TRUE(
StartsWith(response, {kUdpPacketTypeInit, 0x00, U16Msb(sequence), U16Lsb(sequence)}));
// UDP protocol version should be 1.
ASSERT_EQ(ExtractU16(&response[4]), 1);
// Should support at least 512-byte packets or downloads will take forever.
ASSERT_GE(ExtractU16(&response[6]), 512);
}
// Returns true if fastboot is currently in UDP mode, false otherwise.
bool InUdpMode() {
// Reset the flag and poll once, if it gets set back to true we're in UDP.
poll_called_ = false;
fb_bootimg_t bootimg;
fb_poll(&bootimg);
return poll_called_;
}
private:
// Raw C functions to bounce into our active instance.
static void RawPollFunc() { instance_->PollFunc(); }
static int RawSendFunc(const void* data, size_t len, const ip6_addr* daddr, uint16_t dport,
uint16_t sport) {
return instance_->SendFunc(data, len, daddr, dport, sport);
}
// Sends the next packet.
void PollFunc() {
poll_called_ = true;
if (!tx_packets_.empty()) {
std::vector<uint8_t>& packet = tx_packets_.front();
fb_recv(packet.data(), packet.size(), &kTestIpAddress, kTestUdpPort);
tx_packets_.pop();
}
}
int SendFunc(const void* data, size_t len, const ip6_addr* daddr, uint16_t dport,
uint16_t sport) {
EXPECT_EQ(memcmp(daddr, &kTestIpAddress, sizeof(kTestIpAddress)), 0);
EXPECT_EQ(dport, kTestUdpPort);
EXPECT_EQ(sport, FB_SERVER_PORT);
const uint8_t* data_bytes = reinterpret_cast<const uint8_t*>(data);
rx_packets_.emplace(data_bytes, data_bytes + len);
return 0;
}
// Global instance to bounce raw C functions into our object.
static FakeFastbootUdp* instance_;
std::queue<std::vector<uint8_t>> tx_packets_;
std::queue<std::vector<uint8_t>> rx_packets_;
bool poll_called_;
};
FakeFastbootUdp* FakeFastbootUdp::instance_ = nullptr;
// Helper to fake fastboot TCP input and output.
//
// On creation, injects custom TCP functions into fastboot so we can run the
// loop while controlling what gets sent. On deletion, returns fastboot to
// normal functionality.
class FakeFastbootTcp {
public:
FakeFastbootTcp() {
// Fastboot uses the global xefi pointers pretty heavily, replace them with
// our mocks.
gBS = system_table_.BootServices = mock_tcp_.boot_services().services();
gSys = &system_table_;
gImg = ImageHandle();
// Mock out send/receive to inject and receive test data by default.
//
// This is a bit tricky due to the 2-part EFI API; Receive()/Transmit() are
// only called once to initiate the transfer, and CheckEvent() is used from
// then on to poll the status.
//
// To simulate this properly, we need to inject data in the CheckEvent()
// call rather than Receive()/Transmit(), so that we can do things like mock
// data arriving after a few CheckEvent() loops have passed.
ON_CALL(mock_tcp_.client_protocol(), Receive).WillByDefault([this](efi_tcp6_io_token* token) {
receive_token_ = token;
return EFI_SUCCESS;
});
ON_CALL(mock_tcp_.client_protocol(), Transmit).WillByDefault([this](efi_tcp6_io_token* token) {
transmit_token_ = token;
return EFI_SUCCESS;
});
ON_CALL(mock_tcp_.boot_services(), CheckEvent)
.WillByDefault([this](efi_event event) -> efi_status {
if (receive_token_ && receive_token_->CompletionToken.Event == event) {
// Fastboot is polling on Receive().
// Disconnect if requested, otherwise inject any queued test data.
if (disconnect_) {
receive_token_->CompletionToken.Status = EFI_CONNECTION_FIN;
return EFI_SUCCESS;
}
if (tx_data_.empty()) {
return EFI_NOT_READY;
}
size_t size =
std::min(tx_data_.size(), size_t{receive_token_->Packet.RxData->DataLength});
memcpy(receive_token_->Packet.RxData->FragmentTable[0].FragmentBuffer, tx_data_.data(),
size);
receive_token_->Packet.RxData->DataLength = static_cast<uint32_t>(size);
receive_token_->Packet.RxData->FragmentTable[0].FragmentLength =
static_cast<uint32_t>(size);
tx_data_.erase(tx_data_.begin(), tx_data_.begin() + size);
receive_token_->CompletionToken.Status = EFI_SUCCESS;
return EFI_SUCCESS;
}
if (transmit_token_ && transmit_token_->CompletionToken.Event == event) {
// Fastboot is polling on Transmit().
// Disconnect if requested, otherwise save the data it's sending.
if (disconnect_) {
transmit_token_->CompletionToken.Status = EFI_CONNECTION_FIN;
return EFI_SUCCESS;
}
const char* data = reinterpret_cast<const char*>(
transmit_token_->Packet.TxData->FragmentTable[0].FragmentBuffer);
rx_data_.insert(rx_data_.end(), data,
data + transmit_token_->Packet.TxData->DataLength);
transmit_token_->CompletionToken.Status = EFI_SUCCESS;
return EFI_SUCCESS;
}
return EFI_SUCCESS;
});
}
~FakeFastbootTcp() {
gBS = nullptr;
gSys = nullptr;
gImg = nullptr;
fb_reset_tcp_state_for_testing();
// Make sure we used all the expected TCP data.
EXPECT_TRUE(tx_data_.empty());
EXPECT_TRUE(rx_data_.empty());
}
efi::MockBootServices& boot_services() { return mock_tcp_.boot_services(); }
efi::MockTcp6Protocol& server_protocol() { return mock_tcp_.server_protocol(); }
// Injects TCP data into fastboot and runs the loop until it gets pushed
// through.
//
// Returns true if the data was read by fastboot, false if it's still pending
// after running the loop a bunch of times (which may indicate that the
// fastboot loop got into an unexpected state).
bool SendData(std::string_view data) {
tx_data_.insert(tx_data_.end(), data.begin(), data.end());
int poll_count = 0;
while (!tx_data_.empty()) {
fb_bootimg_t bootimg;
fb_poll(&bootimg);
if (++poll_count >= 100) {
return false;
}
}
return true;
}
// Runs the fastboot loop until some TCP data has been received out, and
// returns the resulting data.
//
// Returns empty string instead if nothing has been received after a large
// number of iterations, or if the fb_poll() return value changes.
//
// Args:
// poll_action: if we return early due to non-POLL result and this is
// non-null, it will be filled with the result.
std::string ReceiveData(fb_poll_next_action* poll_action = nullptr) {
int poll_count = 0;
while (rx_data_.empty()) {
fb_bootimg_t bootimg;
fb_poll_next_action result = fb_poll(&bootimg);
if (++poll_count >= 100 || result != POLL) {
if (poll_action) {
*poll_action = result;
}
return "";
}
}
auto ret = std::move(rx_data_);
rx_data_.clear();
return ret;
}
// Sets mock state such that the next fastboot TCP read/write call will
// show that the client has disconnected, then run the loop for a bit to
// flush the state through.
//
// Returns the final fb_poll() return value.
fb_poll_next_action ClientDisconnect() {
// Make sure we have sent all the data we expected to.
EXPECT_TRUE(tx_data_.empty());
disconnect_ = true;
fb_poll_next_action action = POLL;
EXPECT_EQ("", ReceiveData(&action));
return action;
}
void InitializeSession() {
disconnect_ = false;
// Run the initial fastboot loop to initialize TCP, fastboot won't switch
// into TCP mode until the stack is up.
fb_bootimg_t bootimg;
ASSERT_EQ(POLL, fb_poll(&bootimg));
// Now we can tell fastboot to switch to TCP.
fb_tcp_recv();
ASSERT_TRUE(SendData("FB01"));
ASSERT_EQ(ReceiveData(), "FB01");
}
private:
MockTcp mock_tcp_;
efi_system_table system_table_ = {};
// TX/RX data from the perspective of the fastboot client.
std::string tx_data_;
std::string rx_data_;
// Tokens for current in-progress fastboot server receive/transmit calls.
efi_tcp6_io_token* receive_token_ = nullptr;
efi_tcp6_io_token* transmit_token_ = nullptr;
// Set true to mock a fastboot client disconnect.
bool disconnect_ = false;
};
struct GetvarState {
std::unique_ptr<DiskFindBootState> boot_disk_state;
std::unique_ptr<efi::FakeDiskIoProtocol> fake_disk;
};
// Mocks out the necessary EFI commands so that all the getvar functions
// succeed. Without this, trying to run certain getvars will segfault when
// the try to call into a NULL protocol table.
//
// The return value holds the necessary fake disk state and must be kept alive
// until getvar completes.
GetvarState SetupGetvarCommands(efi::MockBootServices& boot_services) {
GetvarState state{.fake_disk = std::make_unique<efi::FakeDiskIoProtocol>()};
// For max-download-size we need to mock out GetMemoryMap.
EXPECT_CALL(boot_services, GetMemoryMap)
.WillOnce([](size_t* memory_map_size, efi_memory_descriptor*, size_t*, size_t*, uint32_t*) {
*memory_map_size = 0;
return EFI_SUCCESS;
});
// For A/B/R slot variables we need to fake a "misc" partition at least big
// enough to hold A/B/R metadata. Contents don't matter, libabr will reset the
// metadata to default state if it doesn't look valid.
state.boot_disk_state = SetupBootDisk(boot_services, state.fake_disk->protocol());
EXPECT_NO_FATAL_FAILURE(SetupDiskPartitions(*state.fake_disk, {kMiscGptEntry}));
return state;
}
TEST(Fastboot, UdpInitializeSession) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(udp.InitializeSession());
}
TEST(Fastboot, UdpInUdpMode) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
EXPECT_TRUE(udp.InUdpMode());
}
TEST(Fastboot, TcpInitializeSession) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
}
TEST(Fastboot, TcpGetvar) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpPacket("getvar:product")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAYgigaboot"));
}
TEST(Fastboot, TcpGetvarAll) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
auto getvar_state = SetupGetvarCommands(tcp.boot_services());
ASSERT_TRUE(tcp.SendData(TcpPacket("getvar:all")));
// Read until we get OKAY. Each intermediate packet should be an INFO
// packet that looks like "INFO<name>:<value>".
std::string reply;
std::set<std::string> replies;
do {
reply = tcp.ReceiveData();
ASSERT_NE(reply, "");
replies.insert(reply);
} while (reply != TcpPacket("OKAY"));
// Check a few variables that we expect.
EXPECT_THAT(replies, Contains(TcpPacket("INFOproduct:gigaboot")));
EXPECT_THAT(replies, Contains(TcpPacket("INFOcurrent-slot:a")));
EXPECT_THAT(replies, Contains(TcpPacket("INFOslot-successful:a:no")));
EXPECT_THAT(replies, Contains(TcpPacket("INFOslot-successful:b:no")));
}
TEST(Fastboot, TcpDownload) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpPacket("download:00000006")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("DATA00000006"));
ASSERT_TRUE(tcp.SendData(TcpPacket("123456")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAY"));
}
TEST(Fastboot, TcpDownloadInParts) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
// Data phase is allowed to break a command up into multiple packets.
ASSERT_TRUE(tcp.SendData(TcpPacket("download:00000006")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("DATA00000006"));
ASSERT_TRUE(tcp.SendData(TcpPacket("123")));
ASSERT_EQ(tcp.ReceiveData(), "");
ASSERT_TRUE(tcp.SendData(TcpPacket("456")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAY"));
}
TEST(Fastboot, TcpMultiCommandSession) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpPacket("getvar:product")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAYgigaboot"));
ASSERT_TRUE(tcp.SendData(TcpPacket("getvar:version")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAY0.4"));
}
// Disconnect immediately after the initialization handshake.
TEST(Fastboot, TcpSessionDisconnectAfterHandshake) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
EXPECT_EQ(tcp.ClientDisconnect(), POLL);
// We should be back in UDP mode after disconnecting.
EXPECT_TRUE(udp.InUdpMode());
}
// Disconnect mid-packet after sending the length but no data.
TEST(Fastboot, TcpSessionDisconnectAfterLength) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpSizeString(4)));
EXPECT_EQ(tcp.ClientDisconnect(), POLL);
EXPECT_TRUE(udp.InUdpMode());
}
// Disconnect after sending a packet but before getting the reply.
TEST(Fastboot, TcpSessionDisconnectAfterReadPacket) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpPacket("getvar:product")));
EXPECT_EQ(tcp.ClientDisconnect(), POLL);
EXPECT_TRUE(udp.InUdpMode());
}
// Disconnect after a full command completes.
TEST(Fastboot, TcpSessionDisconnectAfterCommand) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpPacket("getvar:product")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAYgigaboot"));
EXPECT_EQ(tcp.ClientDisconnect(), POLL);
EXPECT_TRUE(udp.InUdpMode());
}
// Make sure we can repeatedly go into TCP mode.
TEST(Fastboot, TcpMultipleSessions) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpPacket("getvar:product")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAYgigaboot"));
EXPECT_EQ(tcp.ClientDisconnect(), POLL);
EXPECT_TRUE(udp.InUdpMode());
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpPacket("getvar:version")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAY0.4"));
EXPECT_EQ(tcp.ClientDisconnect(), POLL);
EXPECT_TRUE(udp.InUdpMode());
}
// Make sure we re-try opening TCP if it fails the first time due to the
// link not being up yet.
TEST(Fastboot, TcpRetryOpen) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
EXPECT_CALL(tcp.server_protocol(), Configure)
.WillOnce(Return(EFI_INVALID_PARAMETER)) // Fail attempt #1.
.WillOnce(Return(EFI_SUCCESS)); // Succeed attempt #2.
fb_bootimg_t bootimg;
ASSERT_EQ(POLL, fb_poll(&bootimg));
ASSERT_EQ(POLL, fb_poll(&bootimg));
}
TEST(Fastboot, TcpContinue) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpPacket("continue")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAY"));
EXPECT_EQ(tcp.ClientDisconnect(), CONTINUE_BOOT);
}
TEST(Fastboot, TcpReboot) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(tcp.SendData(TcpPacket("reboot")));
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAY"));
EXPECT_EQ(tcp.ClientDisconnect(), REBOOT);
}
TEST(Fastboot, TcpIsAvailable) {
FakeFastbootUdp udp;
FakeFastbootTcp tcp;
// Not available before the drivers have initialized.
ASSERT_FALSE(fb_tcp_is_available());
// Run the loop once, drivers will initialize and TCP should be available.
ASSERT_EQ(POLL, fb_poll(nullptr));
ASSERT_TRUE(fb_tcp_is_available());
// Should continue to be available as we progress through the session and
// after disconnecting.
ASSERT_NO_FATAL_FAILURE(tcp.InitializeSession());
ASSERT_TRUE(fb_tcp_is_available());
ASSERT_TRUE(tcp.SendData(TcpPacket("getvar:product")));
ASSERT_TRUE(fb_tcp_is_available());
ASSERT_EQ(tcp.ReceiveData(), TcpPacket("OKAYgigaboot"));
ASSERT_TRUE(fb_tcp_is_available());
EXPECT_EQ(tcp.ClientDisconnect(), POLL);
ASSERT_TRUE(fb_tcp_is_available());
}
} // namespace
} // namespace gigaboot