| // Copyright 2016 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| #include <lib/elfldltl/machine.h> |
| #include <lib/fidl/txn_header.h> |
| #include <lib/processargs/processargs.h> |
| #include <lib/stdcompat/array.h> |
| #include <lib/stdcompat/source_location.h> |
| #include <lib/stdcompat/span.h> |
| #include <lib/userabi/userboot.h> |
| #include <lib/zircon-internal/default_stack_size.h> |
| #include <lib/zx/channel.h> |
| #include <lib/zx/debuglog.h> |
| #include <lib/zx/job.h> |
| #include <lib/zx/process.h> |
| #include <lib/zx/resource.h> |
| #include <lib/zx/thread.h> |
| #include <lib/zx/time.h> |
| #include <lib/zx/vmar.h> |
| #include <lib/zx/vmo.h> |
| #include <sys/param.h> |
| #include <zircon/fidl.h> |
| #include <zircon/processargs.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/syscalls/log.h> |
| #include <zircon/syscalls/resource.h> |
| #include <zircon/syscalls/system.h> |
| #include <zircon/types.h> |
| |
| #include <array> |
| #include <climits> |
| #include <cstddef> |
| #include <cstdint> |
| #include <cstring> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <type_traits> |
| #include <utility> |
| |
| #include "bootfs.h" |
| #include "loader-service.h" |
| #include "option.h" |
| #include "userboot-elf.h" |
| #include "util.h" |
| #include "zbi.h" |
| |
| namespace { |
| |
| // TODO(https://fxbug.dev/42072759): Replace copy & pasted FIDL C bindings with new C++ bindings |
| // when that's allowed. |
| |
| struct fuchsia_boot_SvcStashStoreRequestMessage { |
| FIDL_ALIGNDECL |
| fidl_message_header_t hdr; |
| zx_handle_t svc_endpoint; |
| }; |
| |
| constexpr uint64_t fuchsia_boot_SvcStashStoreOrdinal = 0xC2648E356CA2870; |
| |
| constexpr const char kStackVmoName[] = "userboot-child-initial-stack"; |
| |
| using namespace userboot; |
| |
| // Reserve roughly the low half of the address space, so the initial |
| // process can use sanitizers that need to allocate shadow memory there. |
| // The reservation VMAR is kept around just long enough to make sure all |
| // the initial allocations (mapping in the initial ELF object, and |
| // allocating the initial stack) stay out of this area, and then destroyed. |
| // The process's own allocations can then use the full address space; if |
| // it's using a sanitizer, it will set up its shadow memory first thing. |
| zx::vmar ReserveLowAddressSpace(const zx::debuglog& log, const zx::vmar& root_vmar) { |
| zx_info_vmar_t info; |
| check(log, root_vmar.get_info(ZX_INFO_VMAR, &info, sizeof(info), nullptr, nullptr), |
| "zx_object_get_info failed on child root VMAR handle"); |
| zx::vmar vmar; |
| uintptr_t addr; |
| size_t reserve_size = (((info.base + info.len) / 2) + zx_system_get_page_size() - 1) & |
| -static_cast<uint64_t>(zx_system_get_page_size()); |
| zx_status_t status = |
| root_vmar.allocate(ZX_VM_SPECIFIC, 0, reserve_size - info.base, &vmar, &addr); |
| check(log, status, "zx_vmar_allocate failed for low address space reservation"); |
| if (addr != info.base) { |
| fail(log, "zx_vmar_allocate gave wrong address?!?"); |
| } |
| return vmar; |
| } |
| |
| void ParseNextProcessArguments(const zx::debuglog& log, std::string_view next, uint32_t& argc, |
| char* argv) { |
| // Extra byte for null terminator. |
| size_t required_size = next.size() + 1; |
| if (required_size > kProcessArgsMaxBytes) { |
| fail(log, "required %zu bytes for process arguments, but only %u are available", required_size, |
| kProcessArgsMaxBytes); |
| } |
| |
| // At a minimum, child processes will be passed a single argument containing the binary name. |
| argc++; |
| uint32_t index = 0; |
| for (char c : next) { |
| if (c == '+') { |
| // Argument list is provided as '+' separated, but passed as null separated. Every time |
| // we encounter a '+' we replace it with a null and increment the argument counter. |
| argv[index] = '\0'; |
| argc++; |
| } else { |
| argv[index] = c; |
| } |
| index++; |
| } |
| |
| argv[index] = '\0'; |
| } |
| |
| // We don't need our own thread handle, but the child does. In addition we pass on a decompressed |
| // BOOTFS VMO, and a debuglog handle (tied to stdout). |
| // |
| // In total we're passing along three more handles than we got. |
| constexpr uint32_t kThreadSelf = kHandleCount; |
| constexpr uint32_t kBootfsVmo = kHandleCount + 1; |
| constexpr uint32_t kDebugLog = kHandleCount + 2; |
| |
| // Hand a svc channel to the child process to be launched. |
| // Fuchsia C runtime will pull this handle and automatically create the endpoint on process startup. |
| constexpr uint32_t kSvcStub = kHandleCount + 3; |
| constexpr uint32_t kSvcNameIndex = 0; |
| |
| // A channel containing all 'svc' stubs server ends. |
| constexpr uint32_t kSvcStash = kHandleCount + 4; |
| |
| constexpr uint32_t kChildHandleCount = kHandleCount + 5; |
| |
| // This is the processargs message the child will receive. |
| struct ChildMessageLayout { |
| zx_proc_args_t header{}; |
| std::array<char, kProcessArgsMaxBytes> args; |
| std::array<uint32_t, kChildHandleCount> info; |
| std::array<char, 5> names = cpp20::to_array("/svc"); |
| }; |
| |
| static_assert(alignof(std::array<uint32_t, kChildHandleCount>) == |
| alignof(uint32_t[kChildHandleCount])); |
| static_assert(alignof(std::array<char, kChildHandleCount>) == alignof(char[kProcessArgsMaxBytes])); |
| |
| constexpr std::array<uint32_t, kChildHandleCount> HandleInfoTable() { |
| std::array<uint32_t, kChildHandleCount> info = {}; |
| // Fill in the handle info table. |
| info[kBootfsVmo] = PA_HND(PA_VMO_BOOTFS, 0); |
| info[kProcSelf] = PA_HND(PA_PROC_SELF, 0); |
| info[kRootJob] = PA_HND(PA_JOB_DEFAULT, 0); |
| info[kRootResource] = PA_HND(PA_RESOURCE, 0); |
| info[kMmioResource] = PA_HND(PA_MMIO_RESOURCE, 0); |
| info[kIrqResource] = PA_HND(PA_IRQ_RESOURCE, 0); |
| #if __x86_64__ |
| info[kIoportResource] = PA_HND(PA_IOPORT_RESOURCE, 0); |
| #elif __aarch64__ |
| info[kSmcResource] = PA_HND(PA_SMC_RESOURCE, 0); |
| #endif |
| info[kSystemResource] = PA_HND(PA_SYSTEM_RESOURCE, 0); |
| info[kThreadSelf] = PA_HND(PA_THREAD_SELF, 0); |
| info[kVmarRootSelf] = PA_HND(PA_VMAR_ROOT, 0); |
| info[kZbi] = PA_HND(PA_VMO_BOOTDATA, 0); |
| for (uint32_t i = kFirstVdso; i <= kLastVdso; ++i) { |
| info[i] = PA_HND(PA_VMO_VDSO, i - kFirstVdso); |
| } |
| for (uint32_t i = kFirstKernelFile; i < kHandleCount; ++i) { |
| info[i] = PA_HND(PA_VMO_KERNEL_FILE, i - kFirstKernelFile); |
| } |
| info[kDebugLog] = PA_HND(PA_FD, kFdioFlagUseForStdio); |
| info[kSvcStub] = PA_HND(PA_NS_DIR, kSvcNameIndex); |
| info[kSvcStash] = PA_HND(PA_USER0, 0); |
| return info; |
| } |
| |
| constexpr ChildMessageLayout CreateChildMessage() { |
| ChildMessageLayout child_message = { |
| .header = |
| { |
| .protocol = ZX_PROCARGS_PROTOCOL, |
| .version = ZX_PROCARGS_VERSION, |
| .handle_info_off = offsetof(ChildMessageLayout, info), |
| .args_off = offsetof(ChildMessageLayout, args), |
| .names_off = offsetof(ChildMessageLayout, names), |
| .names_num = kSvcNameIndex + 1, |
| |
| }, |
| .info = HandleInfoTable(), |
| }; |
| |
| return child_message; |
| } |
| |
| std::array<zx_handle_t, kChildHandleCount> ExtractHandles(zx::channel bootstrap) { |
| // Default constructed debuglog will force check/fail to fallback to |zx_debug_write|. |
| zx::debuglog log; |
| // Read the command line and the essential handles from the kernel. |
| std::array<zx_handle_t, kChildHandleCount> handles = {}; |
| uint32_t actual_handles; |
| zx_status_t status = |
| bootstrap.read(0, nullptr, handles.data(), 0, handles.size(), nullptr, &actual_handles); |
| |
| check(log, status, "cannot read bootstrap message"); |
| if (actual_handles != kHandleCount) { |
| fail(log, "read %u handles instead of %u", actual_handles, kHandleCount); |
| } |
| return handles; |
| } |
| |
| template <typename T> |
| T DuplicateOrDie(const zx::debuglog& log, const T& typed_handle, |
| cpp20::source_location src_loc = cpp20::source_location::current()) { |
| T dup; |
| auto status = typed_handle.duplicate(ZX_RIGHT_SAME_RIGHTS, &dup); |
| check(log, status, "[%s:%d]: Failed to duplicate handle.", src_loc.file_name(), src_loc.line()); |
| return dup; |
| } |
| |
| zx_handle_t RawDuplicateOrDie(const zx::debuglog& log, zx_handle_t handle, |
| cpp20::source_location src_loc = cpp20::source_location::current()) { |
| zx_handle_t dup; |
| auto status = zx_handle_duplicate(handle, ZX_RIGHT_SAME_RIGHTS, &dup); |
| check(log, status, "[%s:%d]: Failed to duplicate handle.", src_loc.file_name(), src_loc.line()); |
| return dup; |
| } |
| |
| zx::debuglog DuplicateOrDie(const zx::debuglog& log, |
| cpp20::source_location src_loc = cpp20::source_location::current()) { |
| return DuplicateOrDie(log, log, src_loc); |
| } |
| |
| struct ChildContext { |
| ChildContext() = default; |
| ChildContext(ChildContext&&) = default; |
| ~ChildContext() { zx_handle_close_many(handles.data(), handles.size()); } |
| |
| // Process creation handles |
| zx::process process; |
| zx::vmar vmar; |
| zx::vmar reserved_vmar; |
| zx::thread thread; |
| |
| zx::channel svc_client; |
| zx::channel svc_server; |
| |
| std::array<zx_handle_t, kChildHandleCount> handles = {}; |
| }; |
| |
| ChildContext CreateChildContext(const zx::debuglog& log, std::string_view name, |
| cpp20::span<const zx_handle_t> handles) { |
| ChildContext child; |
| auto status = |
| zx::process::create(*zx::unowned_job{handles[kRootJob]}, name.data(), |
| static_cast<uint32_t>(name.size()), 0, &child.process, &child.vmar); |
| check(log, status, "Failed to create child process(%.*s).", static_cast<int>(name.length()), |
| name.data()); |
| |
| // Squat on some address space before we start loading it up. |
| child.reserved_vmar = {ReserveLowAddressSpace(log, child.vmar)}; |
| |
| // Create the initial thread in the new process |
| status = zx::thread::create(child.process, name.data(), static_cast<uint32_t>(name.size()), 0, |
| &child.thread); |
| check(log, status, "Failed to create main thread for child process(%.*s).", |
| static_cast<int>(name.length()), name.data()); |
| |
| status = zx::channel::create(0, &child.svc_client, &child.svc_server); |
| check(log, status, "Failed to create svc channels."); |
| |
| // Copy all resources that are not explicitly duplicated in SetChildHandles. |
| for (size_t i = 0; i < handles.size() && i < kHandleCount; ++i) { |
| if (i == kProcSelf) { |
| continue; |
| } |
| |
| if (i == kVmarRootSelf) { |
| continue; |
| } |
| |
| if (handles[i] == ZX_HANDLE_INVALID) { |
| continue; |
| } |
| |
| child.handles[i] = RawDuplicateOrDie(log, handles[i]); |
| } |
| |
| return child; |
| } |
| |
| void SetChildHandles(const zx::debuglog& log, const zx::vmo& bootfs_vmo, ChildContext& child) { |
| child.handles[kBootfsVmo] = DuplicateOrDie(log, bootfs_vmo).release(); |
| child.handles[kDebugLog] = DuplicateOrDie(log).release(); |
| child.handles[kProcSelf] = DuplicateOrDie(log, child.process).release(); |
| child.handles[kVmarRootSelf] = DuplicateOrDie(log, child.vmar).release(); |
| child.handles[kThreadSelf] = DuplicateOrDie(log, child.thread).release(); |
| child.handles[kSvcStub] = child.svc_client.release(); |
| |
| // Verify all child handles. |
| for (size_t i = 0; i < child.handles.size(); ++i) { |
| // The stash handle is only passed to the last process launched by userboot. |
| if (i == kSvcStash) { |
| continue; |
| } |
| auto handle = child.handles[i]; |
| zx_info_handle_basic_t info; |
| auto status = |
| zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); |
| check(log, status, "Failed to obtain handle information. Bad handle at %zu with value %x", i, |
| handle); |
| } |
| } |
| |
| void StashSvc(const zx::debuglog& log, const zx::channel& stash, std::string_view name, |
| zx::channel svc_end) { |
| zx_handle_t h = svc_end.release(); |
| fuchsia_boot_SvcStashStoreRequestMessage request = {}; |
| fidl_init_txn_header(&request.hdr, 0, fuchsia_boot_SvcStashStoreOrdinal, 0); |
| request.svc_endpoint = FIDL_HANDLE_PRESENT; |
| |
| auto status = stash.write(0, &request, sizeof(request), &h, 1); |
| check(log, status, "Failed to stash svc handle from (%.*s)", static_cast<int>(name.length()), |
| name.data()); |
| } |
| |
| void SetStashHandle(const zx::debuglog& log, zx::channel stash, |
| cpp20::span<zx_handle_t, kChildHandleCount> handles) { |
| handles[kSvcStash] = stash.release(); |
| |
| // Check that the handle is valid/alive. |
| zx_info_handle_basic_t info; |
| auto& handle = handles[kSvcStash]; |
| auto status = |
| zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); |
| check(log, status, "Failed to obtain handle information. Bad handle at %d with value %x", |
| static_cast<int>(kSvcStash), handle); |
| } |
| |
| // Set of resources created in userboot. |
| struct Resources { |
| // Needed for properly implementing the epilogue. |
| zx::resource power; |
| |
| // Needed for vending executable memory from bootfs. |
| zx::resource vmex; |
| }; |
| |
| Resources CreateResources(const zx::debuglog& log, |
| cpp20::span<const zx_handle_t, kChildHandleCount> handles) { |
| Resources resources = {}; |
| zx::unowned_resource system(handles[kSystemResource]); |
| auto status = zx::resource::create(*system, ZX_RSRC_KIND_SYSTEM, ZX_RSRC_SYSTEM_POWER_BASE, 1, |
| nullptr, 0, &resources.power); |
| check(log, status, "Failed to created power resource."); |
| |
| status = zx::resource::create(*system, ZX_RSRC_KIND_SYSTEM, ZX_RSRC_SYSTEM_VMEX_BASE, 1, nullptr, |
| 0, &resources.vmex); |
| check(log, status, "Failed to created vmex resource."); |
| return resources; |
| } |
| |
| zx::channel StartChildProcess(const zx::debuglog& log, const Options::ProgramInfo& elf_entry, |
| const ChildMessageLayout& child_message, ChildContext& child, |
| Bootfs& bootfs, size_t handle_count) { |
| size_t stack_size = ZIRCON_DEFAULT_STACK_SIZE; |
| |
| zx::channel to_child, bootstrap; |
| auto status = zx::channel::create(0, &to_child, &bootstrap); |
| check(log, status, "zx_channel_create failed for child stack"); |
| |
| zx::channel loader_svc; |
| |
| // Examine the bootfs image and find the requested file in it. |
| // This will handle a PT_INTERP by doing a second lookup in bootfs. |
| zx_vaddr_t entry = |
| elf_load_bootfs(log, bootfs, elf_entry.root, child.process, child.vmar, child.thread, |
| elf_entry.filename(), to_child, &stack_size, &loader_svc); |
| |
| // Now load the vDSO into the child, so it has access to system calls. |
| zx_vaddr_t vdso_base = |
| elf_load_vdso(log, child.vmar, *zx::unowned_vmo{child.handles[kFirstVdso]}); |
| |
| stack_size = (stack_size + zx_system_get_page_size() - 1) & |
| -static_cast<uint64_t>(zx_system_get_page_size()); |
| zx::vmo stack_vmo; |
| status = zx::vmo::create(stack_size, 0, &stack_vmo); |
| check(log, status, "zx_vmo_create failed for child stack"); |
| stack_vmo.set_property(ZX_PROP_NAME, kStackVmoName, sizeof(kStackVmoName) - 1); |
| zx_vaddr_t stack_base; |
| status = |
| child.vmar.map(ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, 0, stack_vmo, 0, stack_size, &stack_base); |
| check(log, status, "zx_vmar_map failed for child stack"); |
| |
| // Allocate the stack for the child. |
| uintptr_t sp = elfldltl::AbiTraits<>::InitialStackPointer(stack_base, stack_size); |
| printl(log, "stack [%p, %p) sp=%p", reinterpret_cast<void*>(stack_base), |
| reinterpret_cast<void*>(stack_base + stack_size), reinterpret_cast<void*>(sp)); |
| |
| // We're done doing mappings, so clear out the reservation VMAR. |
| check(log, child.reserved_vmar.destroy(), "zx_vmar_destroy failed on reservation VMAR handle"); |
| child.reserved_vmar.reset(); |
| // Now send the bootstrap message. This transfers away all the handles |
| // we have left except the process and thread themselves. |
| status = to_child.write(0, &child_message, sizeof(child_message), child.handles.data(), |
| static_cast<uint32_t>(handle_count)); |
| check(log, status, "zx_channel_write to child failed"); |
| |
| // Start the process going. |
| status = child.process.start(child.thread, entry, sp, std::move(bootstrap), vdso_base); |
| check(log, status, "zx_process_start failed"); |
| child.thread.reset(); |
| |
| return loader_svc; |
| } |
| |
| int64_t WaitForProcessExit(const zx::debuglog& log, const Options::ProgramInfo& entry, |
| const ChildContext& child) { |
| printl(log, "Waiting for %.*s to exit...", static_cast<int>(entry.filename().size()), |
| entry.filename().data()); |
| zx_status_t status = child.process.wait_one(ZX_PROCESS_TERMINATED, zx::time::infinite(), nullptr); |
| check(log, status, "zx_object_wait_one on process failed"); |
| zx_info_process_t info; |
| status = child.process.get_info(ZX_INFO_PROCESS, &info, sizeof(info), nullptr, nullptr); |
| check(log, status, "zx_object_get_info on process failed"); |
| printl(log, "*** Exit status %zd ***\n", info.return_code); |
| return info.return_code; |
| } |
| |
| struct TerminationInfo { |
| // Depending on test mode and result, this might be the return code of boot or test elf. |
| std::optional<int64_t> test_return_code; |
| |
| // Whether we should continue or shutdown. |
| bool should_shutdown = false; |
| |
| zx::resource power; |
| }; |
| |
| [[noreturn]] void HandleTermination(const zx::debuglog& log, const TerminationInfo& info) { |
| if (!info.should_shutdown) { |
| printl(log, "finished!"); |
| zx_process_exit(0); |
| } |
| |
| // The test runners match this exact string on the console log |
| // to determine that the test succeeded since shutting the |
| // machine down doesn't return a value to anyone for us. |
| if (info.test_return_code && info.test_return_code == 0) { |
| printl(log, "%s\n", BOOT_TEST_SUCCESS_STRING); |
| } |
| |
| printl(log, "Process exited. Executing poweroff"); |
| zx_system_powerctl(info.power.get(), ZX_SYSTEM_POWERCTL_SHUTDOWN, nullptr); |
| printl(log, "still here after poweroff!"); |
| |
| while (true) |
| __builtin_trap(); |
| |
| __UNREACHABLE; |
| } |
| |
| // This is the main logic: |
| // 1. Read the kernel's bootstrap message. |
| // 2. Load up the child process from ELF file(s) on the bootfs. |
| // 3. Create the initial thread and allocate a stack for it. |
| // 4. Load up a channel with the zx_proc_args_t message for the child. |
| // 5. Start the child process running. |
| // 6. Optionally, wait for it to exit and then shut down. |
| [[noreturn]] void Bootstrap(zx::channel channel) { |
| // We pass all the same handles the kernel gives us along to the child, |
| // except replacing our own process/root-VMAR handles with its, and |
| // passing along the three extra handles (BOOTFS, thread-self, and a debuglog |
| // handle tied to stdout). |
| std::array<zx_handle_t, kChildHandleCount> handles = ExtractHandles(std::move(channel)); |
| |
| zx::debuglog log; |
| auto status = zx::debuglog::create(*zx::unowned_resource{handles[kRootResource]}, 0, &log); |
| check(log, status, "zx_debuglog_create failed: %d", status); |
| |
| zx::vmar vmar_self{handles[kVmarRootSelf]}; |
| handles[kVmarRootSelf] = ZX_HANDLE_INVALID; |
| |
| zx::process proc_self{handles[kProcSelf]}; |
| handles[kProcSelf] = ZX_HANDLE_INVALID; |
| |
| auto [power, vmex] = CreateResources(log, handles); |
| zx::channel svc_stash_server, svc_stash_client; |
| status = zx::channel::create(0, &svc_stash_server, &svc_stash_client); |
| check(log, status, "Failed to create svc stash channel."); |
| |
| // Locate the ZBI_TYPE_STORAGE_BOOTFS item and decompress it. This will be used to load |
| // the binary referenced by userboot.next, as well as libc. Bootfs will be fully parsed |
| // and hosted under '/boot' either by bootsvc or component manager. |
| const zx::unowned_vmo zbi{handles[kZbi]}; |
| zx::vmo bootfs_vmo = GetBootfsFromZbi(log, vmar_self, *zbi); |
| |
| // Parse CMDLINE items to determine the set of runtime options. |
| Options opts = GetOptionsFromZbi(log, vmar_self, *zbi); |
| |
| TerminationInfo info = { |
| .power = std::move(power), |
| }; |
| |
| { |
| auto borrowed_bootfs = bootfs_vmo.borrow(); |
| Bootfs bootfs{vmar_self.borrow(), std::move(bootfs_vmo), std::move(vmex), DuplicateOrDie(log)}; |
| auto launch_process = [&](auto& elf_entry, |
| zx::channel svc_stash = zx::channel()) -> ChildContext { |
| ChildMessageLayout child_message = CreateChildMessage(); |
| ChildContext child = CreateChildContext(log, elf_entry.filename(), handles); |
| size_t handle_count = kChildHandleCount - 1; |
| |
| StashSvc(log, svc_stash_client, elf_entry.filename(), std::move(child.svc_server)); |
| SetChildHandles(log, *borrowed_bootfs, child); |
| if (svc_stash) { |
| SetStashHandle(log, std::move(svc_stash), child.handles); |
| handle_count++; |
| } |
| |
| // Fill in any '+' separated arguments provided by `userboot.next`. If arguments are longer |
| // than kProcessArgsMaxBytes, this function will fail process creation. |
| ParseNextProcessArguments(log, elf_entry.next, child_message.header.args_num, |
| child_message.args.data()); |
| |
| // Map in the bootfs so we can look for files in it. |
| zx::channel loader_svc = |
| StartChildProcess(log, elf_entry, child_message, child, bootfs, handle_count); |
| printl(log, "process %.*s started.", static_cast<int>(elf_entry.filename().size()), |
| elf_entry.filename().data()); |
| |
| // Now become the loader service for as long as that's needed. |
| if (loader_svc) { |
| LoaderService ldsvc(DuplicateOrDie(log), &bootfs, elf_entry.root); |
| ldsvc.Serve(std::move(loader_svc)); |
| } |
| |
| return child; |
| }; |
| |
| if (!opts.test.next.empty()) { |
| // If no boot, then hand over the stash to the test program. Test does not get the svc stash. |
| auto test_context = launch_process(opts.test); |
| // Wait for test to finish. |
| info.test_return_code = WaitForProcessExit(log, opts.test, test_context); |
| |
| info.should_shutdown = opts.boot.next.empty(); |
| } |
| |
| if (!opts.boot.next.empty()) { |
| [[maybe_unused]] auto boot_context = launch_process(opts.boot, std::move(svc_stash_server)); |
| } |
| } |
| HandleTermination(log, info); |
| } |
| |
| } // anonymous namespace |
| |
| // This is the entry point for the whole show, the very first bit of code |
| // to run in user mode. |
| extern "C" [[noreturn]] void _start(zx_handle_t arg) { Bootstrap(zx::channel{arg}); } |