| // Copyright 2016 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 "src/bringup/bin/netsvc/netboot.h" |
| |
| #include <assert.h> |
| #include <errno.h> |
| #include <fidl/fuchsia.boot/cpp/wire.h> |
| #include <fidl/fuchsia.hardware.power.statecontrol/cpp/wire.h> |
| #include <lib/component/incoming/cpp/protocol.h> |
| #include <lib/netboot/netboot.h> |
| #include <lib/zx/vmo.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <threads.h> |
| #include <zircon/process.h> |
| #include <zircon/processargs.h> |
| #include <zircon/syscalls.h> |
| |
| #include "src/bringup/bin/netsvc/inet6.h" |
| #include "src/bringup/bin/netsvc/netcp.h" |
| #include "src/bringup/bin/netsvc/netsvc.h" |
| #include "src/bringup/bin/netsvc/paver.h" |
| #include "src/bringup/bin/netsvc/zbi.h" |
| |
| bool xfer_active = false; |
| |
| namespace { |
| |
| uint32_t last_cookie = 0; |
| uint32_t last_cmd = 0; |
| uint32_t last_arg = 0; |
| uint32_t last_ack_cmd = 0; |
| uint32_t last_ack_arg = 0; |
| |
| #define MAX_ADVERTISE_DATA_LEN 256 |
| |
| struct nbfilecontainer_t { |
| nbfile_t file; |
| zx_handle_t data; // handle to vmo that backs netbootfile. |
| }; |
| |
| nbfilecontainer_t nbzbi; |
| nbfilecontainer_t nbcmdline; |
| |
| // Pointer to the currently active transfer. |
| nbfile_t* active; |
| |
| namespace statecontrol = fuchsia_hardware_power_statecontrol; |
| |
| zx_status_t nbfilecontainer_init(size_t size, nbfilecontainer_t* target) { |
| zx_status_t st = ZX_OK; |
| |
| assert(target); |
| |
| // De-init the container if it's already initialized. |
| if (target->file.data) { |
| // For now, I can't see a valid reason that a client would send the same |
| // filename twice. |
| // We'll handle this case gracefully, but we'll print a warning to the |
| // console just in case it was a mistake. |
| printf("netbootloader: warning, reusing a previously initialized container\n"); |
| |
| // Unmap the vmo from the address space. |
| st = zx_vmar_unmap(zx_vmar_root_self(), reinterpret_cast<uintptr_t>(target->file.data), |
| target->file.size); |
| if (st != ZX_OK) { |
| printf("netbootloader: failed to unmap existing vmo, st = %d\n", st); |
| return st; |
| } |
| |
| zx_handle_close(target->data); |
| |
| target->file.offset = 0; |
| target->file.size = 0; |
| target->file.data = nullptr; |
| } |
| |
| st = zx_vmo_create(size, 0, &target->data); |
| if (st != ZX_OK) { |
| printf( |
| "netbootloader: Could not create a netboot vmo of size = %lu " |
| "retcode = %d\n", |
| size, st); |
| return st; |
| } |
| zx_object_set_property(target->data, ZX_PROP_NAME, "netboot", 7); |
| |
| uintptr_t buffer; |
| st = zx_vmar_map(zx_vmar_root_self(), ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, 0, target->data, 0, |
| size, &buffer); |
| if (st != ZX_OK) { |
| printf("netbootloader: failed to map data vmo for buffer, st = %d\n", st); |
| zx_handle_close(target->data); |
| return st; |
| } |
| |
| target->file.offset = 0; |
| target->file.size = size; |
| target->file.data = reinterpret_cast<uint8_t*>(buffer); |
| |
| return ZX_OK; |
| } |
| |
| } // namespace |
| |
| nbfile_t* netboot_get_buffer(const char* name, size_t size) { |
| zx_status_t st = ZX_OK; |
| nbfilecontainer_t* result; |
| |
| if (!strcmp(name, NETBOOT_KERNEL_FILENAME)) { |
| result = &nbzbi; |
| } else if (!strcmp(name, NETBOOT_CMDLINE_FILENAME)) { |
| result = &nbcmdline; |
| } else { |
| return nullptr; |
| } |
| |
| st = nbfilecontainer_init(size, result); |
| if (st != ZX_OK) { |
| printf( |
| "netbootloader: failed to initialize file container for " |
| "file = '%s', retcode = %d\n", |
| name, st); |
| return nullptr; |
| } |
| |
| return &result->file; |
| } |
| |
| void netboot_advertise(const char* nodename) { |
| // Don't advertise if a transfer is active. |
| if (xfer_active) |
| return; |
| |
| struct { |
| netboot_message_header_t hdr; |
| char data[MAX_ADVERTISE_DATA_LEN]; |
| } packet_data; |
| netboot_message_header_t* hdr = &packet_data.hdr; |
| hdr->magic = NETBOOT_MAGIC; |
| hdr->cookie = 0; |
| hdr->cmd = NETBOOT_COMMAND_ADVERTISE; |
| hdr->arg = NETBOOT_VERSION_CURRENT; |
| |
| snprintf(packet_data.data, MAX_ADVERTISE_DATA_LEN, "version=%s;nodename=%s", |
| NETBOOT_BOOTLOADER_VERSION, nodename); |
| const size_t data_len = strlen(packet_data.data) + 1; |
| udp6_send(&packet_data, sizeof(netboot_message_header_t) + data_len, &ip6_ll_all_nodes, |
| NETBOOT_PORT_ADVERT, NETBOOT_PORT_SERVER, false); |
| } |
| |
| namespace { |
| |
| void nb_open(const char* filename, uint32_t cookie, uint32_t arg, const ip6_addr_t* saddr, |
| uint16_t sport, uint16_t dport) { |
| netboot_message_header_t m; |
| m.magic = NETBOOT_MAGIC; |
| m.cookie = cookie; |
| m.cmd = NETBOOT_COMMAND_ACK; |
| m.arg = netcp_open(filename, arg, nullptr); |
| udp6_send(&m, sizeof(m), saddr, sport, dport, false); |
| } |
| |
| struct netfilemsg { |
| netboot_message_header_t hdr; |
| uint8_t data[1024]; |
| }; |
| |
| void nb_read(uint32_t cookie, uint32_t arg, const ip6_addr_t* saddr, uint16_t sport, |
| uint16_t dport) { |
| static netfilemsg m = { |
| .hdr = |
| { |
| .magic = NETBOOT_MAGIC, |
| .cookie = 0, |
| .cmd = NETBOOT_COMMAND_ACK, |
| .arg = 0, |
| }, |
| .data = {}, |
| }; |
| static size_t msg_size = 0; |
| static uint32_t blocknum = static_cast<uint32_t>(-1); |
| if (arg == blocknum) { |
| // Request to resend last message, verify that the cookie is unchanged |
| if (cookie != m.hdr.cookie) { |
| m.hdr.arg = -EIO; |
| m.hdr.cookie = cookie; |
| msg_size = sizeof(m.hdr); |
| } |
| } else if (arg == 0 || arg == blocknum + 1) { |
| ssize_t result = netcp_read(&m.data, sizeof(m.data)); |
| if (result < 0) { |
| m.hdr.arg = static_cast<uint32_t>(result); |
| msg_size = sizeof(m.hdr); |
| } else { |
| // Note that the response does not use actual size as the argument, |
| // it matches the *requested* size. Actual size can be determined by |
| // the packet size. |
| m.hdr.arg = arg; |
| msg_size = sizeof(m.hdr) + result; |
| } |
| m.hdr.cookie = cookie; |
| blocknum = arg; |
| } else { |
| // Ignore bogus read requests -- host will timeout if they're confused |
| return; |
| } |
| udp6_send(&m, msg_size, saddr, sport, dport, false); |
| } |
| |
| void nb_write(const char* data, size_t len, uint32_t cookie, uint32_t arg, const ip6_addr_t* saddr, |
| uint16_t sport, uint16_t dport) { |
| static netboot_message_header_t m = { |
| .magic = NETBOOT_MAGIC, |
| .cookie = 0, |
| .cmd = NETBOOT_COMMAND_ACK, |
| .arg = 0, |
| }; |
| static uint32_t blocknum = static_cast<uint32_t>(-1); |
| if (arg == blocknum) { |
| // Request to repeat last write, verify that cookie is unchanged |
| if (cookie != m.cookie) { |
| m.arg = -EIO; |
| } |
| } else if (arg == 0 || arg == blocknum + 1) { |
| ssize_t result = netcp_write(data, len); |
| m.arg = static_cast<uint32_t>(result > 0 ? 0 : result); |
| blocknum = arg; |
| } |
| m.cookie = cookie; |
| udp6_send(&m, sizeof(m), saddr, sport, dport, false); |
| } |
| |
| void nb_close(uint32_t cookie, const ip6_addr_t* saddr, uint16_t sport, uint16_t dport) { |
| netboot_message_header_t m; |
| m.magic = NETBOOT_MAGIC; |
| m.cookie = cookie; |
| m.cmd = NETBOOT_COMMAND_ACK; |
| m.arg = netcp_close(); |
| udp6_send(&m, sizeof(m), saddr, sport, dport, false); |
| } |
| |
| zx_status_t do_dmctl_mexec() { |
| zx::vmo kernel_zbi, data_zbi; |
| // TODO(scottmg): range check nbcmdline.file.size rather than just casting. |
| zx_status_t status = netboot_prepare_zbi( |
| zx::vmo(nbzbi.data), |
| std::string_view(reinterpret_cast<const char*>(nbcmdline.file.data), nbcmdline.file.size), |
| &kernel_zbi, &data_zbi); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| zx::result client_end = component::Connect<statecontrol::Admin>(); |
| if (client_end.is_error()) { |
| return client_end.status_value(); |
| } |
| fidl::WireResult response = |
| fidl::WireCall(client_end.value())->Mexec(std::move(kernel_zbi), std::move(data_zbi)); |
| if (response.status() != ZX_OK) { |
| return response.status(); |
| } |
| if (response.value().is_error()) { |
| return response.value().error_value(); |
| } |
| // Wait for the world to end. |
| zx_nanosleep(ZX_TIME_INFINITE); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| zx_status_t reboot() { |
| zx::result client_end = component::Connect<statecontrol::Admin>(); |
| if (client_end.is_error()) { |
| return client_end.status_value(); |
| } |
| fidl::WireResult response = |
| fidl::WireCall(client_end.value())->Reboot(statecontrol::wire::RebootReason::kUserRequest); |
| if (response.status() != ZX_OK) { |
| return response.status(); |
| } |
| if (response.value().is_error()) { |
| return response.value().error_value(); |
| } |
| // Wait for the world to end. |
| zx_nanosleep(ZX_TIME_INFINITE); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| void bootloader_recv(void* data, size_t len, const ip6_addr_t* daddr, uint16_t dport, |
| const ip6_addr_t* saddr, uint16_t sport) { |
| netboot_message_header_t msg_header; |
| netboot_message_header_t ack; |
| |
| bool do_transmit = true; |
| bool do_boot = false; |
| bool do_reboot = false; |
| |
| if (dport != NETBOOT_PORT_SERVER) |
| return; |
| |
| // Not enough bytes to be a message |
| if (len < sizeof(netboot_message_header_t)) |
| return; |
| |
| // Must be copied to stack structure rather than interpreted in place for |
| // UBSan, see https://fxbug.dev/42122166. |
| memcpy(&msg_header, data, sizeof(msg_header)); |
| char* msg_data = reinterpret_cast<char*>(data) + sizeof(msg_header); |
| |
| len -= sizeof(netboot_message_header_t); |
| |
| if ((last_cookie == msg_header.cookie) && (last_cmd == msg_header.cmd) && |
| (last_arg == msg_header.arg)) { |
| // host must have missed the ack. resend |
| ack.magic = NETBOOT_MAGIC; |
| ack.cookie = last_cookie; |
| ack.cmd = last_ack_cmd; |
| ack.arg = last_ack_arg; |
| goto transmit; |
| } |
| |
| ack.cmd = NETBOOT_COMMAND_ACK; |
| ack.arg = 0; |
| |
| switch (msg_header.cmd) { |
| case NETBOOT_COMMAND_EXECUTE: |
| if (len == 0) |
| return; |
| msg_data[len - 1] = 0; |
| break; |
| case NETBOOT_COMMAND_SEND_FILE: |
| xfer_active = true; |
| if (len == 0) |
| return; |
| msg_data[len - 1] = 0; |
| for (size_t i = 0; i < (len - 1); i++) { |
| if ((msg_data[i] < ' ') || (msg_data[i] > 127)) { |
| msg_data[i] = '.'; |
| } |
| } |
| active = netboot_get_buffer(msg_data, msg_header.arg); |
| if (active) { |
| active->offset = 0; |
| ack.arg = msg_header.arg; |
| size_t prefix_len = strlen(NETBOOT_FILENAME_PREFIX); |
| const char* filename; |
| if (!strncmp(msg_data, NETBOOT_FILENAME_PREFIX, prefix_len)) { |
| filename = &(reinterpret_cast<const char*>(msg_data))[prefix_len]; |
| } else { |
| filename = reinterpret_cast<const char*>(msg_data); |
| } |
| printf("netboot: Receive File '%s'...\n", filename); |
| } else { |
| printf("netboot: Rejected File '%s'...\n", msg_data); |
| ack.cmd = NETBOOT_COMMAND_ERROR_BAD_FILE; |
| } |
| break; |
| |
| case NETBOOT_COMMAND_DATA: |
| case NETBOOT_COMMAND_LAST_DATA: |
| xfer_active = true; |
| if (active == nullptr) { |
| printf("netboot: > received chunk before NETBOOT_FILE\n"); |
| return; |
| } |
| if (msg_header.arg != active->offset) { |
| // printf("netboot: < received chunk at offset %d but current offset is %zu\n", |
| // msg_header.arg, active->offset); |
| ack.arg = static_cast<uint32_t>(active->offset); |
| ack.cmd = NETBOOT_COMMAND_ACK; |
| } else if ((active->offset + len) > active->size) { |
| ack.cmd = NETBOOT_COMMAND_ERROR_TOO_LARGE; |
| ack.arg = msg_header.arg; |
| } else { |
| memcpy(active->data + active->offset, msg_data, len); |
| active->offset += len; |
| ack.cmd = msg_header.cmd == NETBOOT_COMMAND_LAST_DATA ? NETBOOT_COMMAND_FILE_RECEIVED |
| : NETBOOT_COMMAND_ACK; |
| if (msg_header.cmd != NETBOOT_COMMAND_LAST_DATA) { |
| do_transmit = false; |
| } else { |
| xfer_active = false; |
| } |
| } |
| break; |
| case NETBOOT_COMMAND_BOOT: |
| // Wait for the paver to complete if it is running. |
| if (zx_status_t exit_code = netsvc::Paver::Get().exit_code().get(); exit_code != ZX_OK) { |
| printf("netboot: detected paver error: %s\n", zx_status_get_string(exit_code)); |
| break; |
| } |
| do_boot = true; |
| printf("netboot: Boot Kernel...\n"); |
| break; |
| case NETBOOT_COMMAND_REBOOT: |
| // Wait for the paver to complete if it is running. |
| if (zx_status_t exit_code = netsvc::Paver::Get().exit_code().get(); exit_code != ZX_OK) { |
| printf("netboot: detected paver error: %s\n", zx_status_get_string(exit_code)); |
| break; |
| } |
| do_reboot = true; |
| printf("netboot: Reboot ...\n"); |
| break; |
| default: |
| // We don't have a handler for this command, let netsvc handle it. |
| do_transmit = false; |
| } |
| |
| last_cookie = msg_header.cookie; |
| last_cmd = msg_header.cmd; |
| last_arg = msg_header.arg; |
| last_ack_cmd = ack.cmd; |
| last_ack_arg = ack.arg; |
| |
| ack.cookie = msg_header.cookie; |
| ack.magic = NETBOOT_MAGIC; |
| transmit: |
| if (do_transmit) { |
| udp6_send(&ack, sizeof(ack), saddr, sport, NETBOOT_PORT_SERVER, false); |
| } |
| |
| if (do_boot) { |
| zx_status_t status = do_dmctl_mexec(); |
| if (status != ZX_OK) { |
| // TODO: This will return before the system actually mexecs. |
| // We can't pass an event to wait on here because fdio |
| // has a limit of 3 handles, and we're already using |
| // all 3 to pass boot parameters. |
| printf("netboot: Boot failed. status = %d\n", status); |
| } |
| } |
| |
| if (do_reboot) { |
| zx_status_t status = reboot(); |
| if (status != ZX_OK) { |
| printf("netboot: Reboot failed. status = %d\n", status); |
| } |
| } |
| } |
| |
| } // namespace |
| |
| void netboot_recv(void* data, size_t len, bool is_mcast, const ip6_addr_t* daddr, uint16_t dport, |
| const ip6_addr_t* saddr, uint16_t sport) { |
| netboot_message_header_t msg_header; |
| // Not enough bytes to be a message |
| if (len < sizeof(msg_header)) { |
| return; |
| } |
| // Must be copied to stack structure rather than interpreted in place for |
| // UBSan, see https://fxbug.dev/42122166. |
| memcpy(&msg_header, data, sizeof(msg_header)); |
| char* msg_data = reinterpret_cast<char*>(data) + sizeof(msg_header); |
| |
| if (msg_header.magic != NETBOOT_MAGIC) { |
| return; |
| } |
| len -= sizeof(msg_header); |
| |
| if (len && msg_header.cmd != NETBOOT_COMMAND_DATA && |
| msg_header.cmd != NETBOOT_COMMAND_LAST_DATA) { |
| msg_data[len - 1] = '\0'; |
| } |
| |
| if (!all_features()) { |
| assert(!netbootloader()); // This is checked in netsvc's main(). |
| if (msg_header.cmd != NETBOOT_COMMAND_QUERY) { |
| // Only QUERY is allowed in minimal mode. |
| return; |
| } |
| } |
| |
| switch (msg_header.cmd) { |
| case NETBOOT_COMMAND_QUERY: { |
| std::string_view msg_data_view{msg_data}; |
| if (msg_data_view != "*" && msg_data_view != nodename()) { |
| break; |
| } |
| size_t dlen = strlen(nodename()) + 1; |
| char buf[1024 + sizeof(netboot_message_header_t)]; |
| if ((dlen + sizeof(netboot_message_header_t)) > sizeof(buf)) { |
| return; |
| } |
| msg_header.cmd = NETBOOT_COMMAND_ACK; |
| memcpy(buf, &msg_header, sizeof(netboot_message_header_t)); |
| memcpy(buf + sizeof(netboot_message_header_t), nodename(), dlen); |
| udp6_send(buf, sizeof(netboot_message_header_t) + dlen, saddr, sport, dport, false); |
| break; |
| } |
| case NETBOOT_COMMAND_GET_ADVERT: { |
| // Only respond when netbootloader is enabled, so that paving behavior is same as the |
| // broadcasted advertisement. |
| if (!netbootloader()) { |
| break; |
| } |
| |
| struct { |
| netboot_message_header_t hdr; |
| char data[MAX_ADVERTISE_DATA_LEN]; |
| } packet_data; |
| netboot_message_header_t* hdr = &packet_data.hdr; |
| hdr->magic = NETBOOT_MAGIC; |
| hdr->cookie = 0; |
| hdr->cmd = NETBOOT_COMMAND_ADVERTISE; |
| hdr->arg = NETBOOT_VERSION_CURRENT; |
| |
| snprintf(packet_data.data, MAX_ADVERTISE_DATA_LEN, "version=%s;nodename=%s", |
| NETBOOT_BOOTLOADER_VERSION, nodename()); |
| const size_t data_len = strlen(packet_data.data) + 1; |
| udp6_send(&packet_data, sizeof(netboot_message_header_t) + data_len, saddr, sport, dport, |
| false); |
| break; |
| } |
| case NETBOOT_COMMAND_SHELL_CMD: |
| if (!is_mcast) { |
| netboot_run_cmd(msg_data); |
| return; |
| } |
| break; |
| case NETBOOT_COMMAND_OPEN: |
| nb_open(msg_data, msg_header.cookie, msg_header.arg, saddr, sport, dport); |
| break; |
| case NETBOOT_COMMAND_READ: |
| nb_read(msg_header.cookie, msg_header.arg, saddr, sport, dport); |
| break; |
| case NETBOOT_COMMAND_WRITE: |
| len--; // NB NUL-terminator is not part of the data |
| nb_write(msg_data, len, msg_header.cookie, msg_header.arg, saddr, sport, dport); |
| break; |
| case NETBOOT_COMMAND_CLOSE: |
| nb_close(msg_header.cookie, saddr, sport, dport); |
| break; |
| default: |
| // If the bootloader is enabled, then let it have a crack at the |
| // incoming packets as well. |
| if (netbootloader()) { |
| bootloader_recv(data, len + sizeof(netboot_message_header_t), daddr, dport, saddr, sport); |
| } |
| } |
| } |