|  | // Copyright 2019 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/virtualization/bin/guest/vshc.h" | 
|  |  | 
|  | #include <fcntl.h> | 
|  | #include <fuchsia/hardware/pty/llcpp/fidl.h> | 
|  | #include <fuchsia/virtualization/cpp/fidl.h> | 
|  | #include <lib/syslog/cpp/macros.h> | 
|  | #include <poll.h> | 
|  | #include <unistd.h> | 
|  | #include <zircon/status.h> | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <iostream> | 
|  |  | 
|  | #include <google/protobuf/message_lite.h> | 
|  |  | 
|  | #include "src/lib/fsl/socket/socket_drainer.h" | 
|  | #include "src/lib/fsl/tasks/fd_waiter.h" | 
|  | #include "src/virtualization/lib/vsh/util.h" | 
|  | #include "src/virtualization/packages/biscotti_guest/third_party/protos/vsh.pb.h" | 
|  |  | 
|  | namespace fpty = ::llcpp::fuchsia::hardware::pty; | 
|  |  | 
|  | std::pair<int, int> init_tty(void) { | 
|  | int cols = 80; | 
|  | int rows = 24; | 
|  |  | 
|  | if (isatty(STDIN_FILENO)) { | 
|  | fdio_t* io = fdio_unsafe_fd_to_io(STDIN_FILENO); | 
|  | auto wsz = | 
|  | fpty::Device::Call::GetWindowSize(zx::unowned_channel(fdio_unsafe_borrow_channel(io))); | 
|  |  | 
|  | if (wsz.status() != ZX_OK || wsz->status != ZX_OK) { | 
|  | FX_LOGS(WARNING) << "Unable to determine shell geometry, defaulting to 80x24"; | 
|  | } else { | 
|  | cols = wsz->size.width; | 
|  | rows = wsz->size.height; | 
|  | } | 
|  |  | 
|  | // Enable raw mode on tty so that inputs such as ctrl-c are passed on | 
|  | // faithfully to the client for forwarding to the remote shell | 
|  | // (instead of closing the client side). | 
|  | auto result = fpty::Device::Call::ClrSetFeature( | 
|  | zx::unowned_channel(fdio_unsafe_borrow_channel(io)), 0, fpty::FEATURE_RAW); | 
|  |  | 
|  | if (result.status() != ZX_OK || result->status != ZX_OK) { | 
|  | FX_LOGS(ERROR) << "Failed to set FEATURE_RAW, some features may not work."; | 
|  | } | 
|  |  | 
|  | fdio_unsafe_release(io); | 
|  | } | 
|  |  | 
|  | return {cols, rows}; | 
|  | } | 
|  |  | 
|  | void reset_tty(void) { | 
|  | if (isatty(STDIN_FILENO)) { | 
|  | fdio_t* io = fdio_unsafe_fd_to_io(STDIN_FILENO); | 
|  | auto result = fpty::Device::Call::ClrSetFeature( | 
|  | zx::unowned_channel(fdio_unsafe_borrow_channel(io)), fpty::FEATURE_RAW, 0); | 
|  |  | 
|  | if (result.status() != ZX_OK || result->status != ZX_OK) { | 
|  | FX_LOGS(ERROR) << "Failed to reset FEATURE_RAW"; | 
|  | } | 
|  |  | 
|  | fdio_unsafe_release(io); | 
|  | } | 
|  | } | 
|  |  | 
|  | namespace { | 
|  | class ConsoleIn { | 
|  | public: | 
|  | ConsoleIn(async::Loop* loop, zx::unowned_socket&& socket) | 
|  | : loop_(loop), sink_(std::move(socket)), fd_waiter_(loop->dispatcher()) {} | 
|  |  | 
|  | bool Start(void) { | 
|  | if (fcntl(STDIN_FILENO, F_GETFD) != -1) { | 
|  | fd_waiter_.Wait(fit::bind_member(this, &ConsoleIn::HandleStdin), STDIN_FILENO, POLLIN); | 
|  | } else { | 
|  | FX_LOGS(ERROR) << "Unable to start the async output loop"; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void HandleStdin(zx_status_t status, uint32_t events) { | 
|  | if (status != ZX_OK && status != ZX_ERR_SHOULD_WAIT) { | 
|  | loop_->Shutdown(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | vm_tools::vsh::GuestMessage msg_out; | 
|  | uint8_t buf[vsh::kMaxDataSize]; | 
|  | ssize_t actual = read(STDIN_FILENO, buf, vsh::kMaxDataSize); | 
|  |  | 
|  | msg_out.mutable_data_message()->set_stream(vm_tools::vsh::STDIN_STREAM); | 
|  | msg_out.mutable_data_message()->set_data(buf, actual); | 
|  | if (!vsh::SendMessage(*sink_, msg_out)) { | 
|  | FX_LOGS(ERROR) << "Failed to send stdin"; | 
|  | return; | 
|  | } | 
|  |  | 
|  | fd_waiter_.Wait(fit::bind_member(this, &ConsoleIn::HandleStdin), STDIN_FILENO, POLLIN); | 
|  | } | 
|  |  | 
|  | private: | 
|  | async::Loop* loop_; | 
|  | zx::unowned_socket sink_; | 
|  | fsl::FDWaiter fd_waiter_; | 
|  | }; | 
|  |  | 
|  | class ConsoleOut { | 
|  | public: | 
|  | ConsoleOut(async::Loop* loop, zx::unowned_socket&& socket) | 
|  | : loop_(loop), | 
|  | source_(std::move(socket)), | 
|  | reading_size_(true), | 
|  | msg_size_(sizeof(uint32_t)), | 
|  | bytes_left_(msg_size_) {} | 
|  |  | 
|  | bool Start(void) { | 
|  | wait_.set_object(source_->get()); | 
|  | wait_.set_trigger(ZX_SOCKET_READABLE); | 
|  | auto status = wait_.Begin(loop_->dispatcher()); | 
|  | if (status != ZX_OK) { | 
|  | FX_LOGS(ERROR) << "Unable to start the async input loop"; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void HandleTtyOutput(async_dispatcher_t* dispatcher, async::WaitBase* wait, zx_status_t status, | 
|  | const zx_packet_signal_t* signal) { | 
|  | if (status != ZX_OK && status != ZX_ERR_SHOULD_WAIT) { | 
|  | loop_->Shutdown(); | 
|  | loop_->Quit(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | vm_tools::vsh::HostMessage msg_in; | 
|  |  | 
|  | if (status != ZX_ERR_SHOULD_WAIT && bytes_left_) { | 
|  | size_t actual; | 
|  | source_->read(0, buf_ + (msg_size_ - bytes_left_), bytes_left_, &actual); | 
|  | bytes_left_ -= actual; | 
|  | } | 
|  |  | 
|  | if (bytes_left_ == 0 && reading_size_) { | 
|  | uint32_t sz; | 
|  | std::memcpy(&sz, buf_, sizeof(sz)); | 
|  |  | 
|  | // set state for next iteration | 
|  | reading_size_ = false; | 
|  | msg_size_ = le32toh(sz); | 
|  | bytes_left_ = msg_size_; | 
|  |  | 
|  | FX_CHECK(msg_size_ <= vsh::kMaxMessageSize) | 
|  | << "Message size of " << msg_size_ << " exceeds kMaxMessageSize"; | 
|  |  | 
|  | } else if (bytes_left_ == 0 && !reading_size_) { | 
|  | FX_CHECK(msg_in.ParseFromArray(buf_, msg_size_)) << "Failed to parse incoming message."; | 
|  |  | 
|  | reading_size_ = true; | 
|  | msg_size_ = sizeof(uint32_t); | 
|  | bytes_left_ = msg_size_; | 
|  |  | 
|  | switch (msg_in.msg_case()) { | 
|  | case vm_tools::vsh::HostMessage::MsgCase::kDataMessage: { | 
|  | auto data = msg_in.data_message().data(); | 
|  | write(STDOUT_FILENO, data.data(), data.size()); | 
|  | } break; | 
|  | case vm_tools::vsh::HostMessage::MsgCase::kStatusMessage: | 
|  | if (msg_in.status_message().status() != vm_tools::vsh::READY) { | 
|  | loop_->Shutdown(); | 
|  | loop_->Quit(); | 
|  | reset_tty(); | 
|  | if (msg_in.status_message().status() == vm_tools::vsh::EXITED) { | 
|  | exit(msg_in.status_message().code()); | 
|  | } else { | 
|  | FX_LOGS(ERROR) << "vsh did not complete successfully."; | 
|  | exit(-1); | 
|  | } | 
|  | } | 
|  | break; | 
|  | default: | 
|  | FX_LOGS(WARNING) << "Unhandled HostMessage received."; | 
|  | } | 
|  | } | 
|  |  | 
|  | status = wait->Begin(dispatcher); | 
|  | } | 
|  |  | 
|  | private: | 
|  | async::WaitMethod<ConsoleOut, &ConsoleOut::HandleTtyOutput> wait_{this}; | 
|  | async::Loop* loop_; | 
|  | zx::unowned_socket source_; | 
|  |  | 
|  | uint8_t buf_[vsh::kMaxMessageSize]; | 
|  | bool reading_size_; | 
|  | size_t msg_size_; | 
|  | size_t bytes_left_; | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | static bool init_shell(const zx::socket& usock) { | 
|  | vm_tools::vsh::SetupConnectionRequest conn_req; | 
|  | vm_tools::vsh::SetupConnectionResponse conn_resp; | 
|  |  | 
|  | // Target can be |vsh::kVmShell| or the empty string for the VM. | 
|  | // Specifying container name directly here is not supported. | 
|  | conn_req.set_target(""); | 
|  | // User can be defaulted with empty string. This is chronos for vmshell and | 
|  | // root otherwise | 
|  | conn_req.set_user(""); | 
|  | // Blank command for login shell. (other uses deprecated, use argv directly | 
|  | // instead) | 
|  | conn_req.set_command(""); | 
|  | conn_req.clear_argv(); | 
|  |  | 
|  | auto env = conn_req.mutable_env(); | 
|  | if (auto term_env = getenv("TERM")) | 
|  | (*env)["TERM"] = std::string(term_env); | 
|  |  | 
|  | (*env)["LXD_DIR"] = "/mnt/stateful/lxd"; | 
|  | (*env)["LXD_CONF"] = "/mnt/stateful/lxd_conf"; | 
|  | (*env)["LXD_UNPRIVILEGED_ONLY"] = "true"; | 
|  |  | 
|  | if (!vsh::SendMessage(usock, conn_req)) { | 
|  | FX_LOGS(ERROR) << "Failed to send connection request"; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // No use setting up the async message handling if we haven't even | 
|  | // connected properly. Block on connection response. | 
|  | if (!vsh::RecvMessage(usock, &conn_resp)) { | 
|  | FX_LOGS(ERROR) << "Failed to receive response from vshd, giving up after one try"; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (conn_resp.status() != vm_tools::vsh::READY) { | 
|  | FX_LOGS(ERROR) << "Server was unable to set up connection properly: " | 
|  | << conn_resp.description(); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Connection to server established. | 
|  | // Initial Configuration Phase. | 
|  | auto [cols, rows] = init_tty(); | 
|  | vm_tools::vsh::GuestMessage msg_out; | 
|  | msg_out.mutable_resize_message()->set_cols(cols); | 
|  | msg_out.mutable_resize_message()->set_rows(rows); | 
|  | if (!vsh::SendMessage(usock, msg_out)) { | 
|  | FX_LOGS(ERROR) << "Failed to send window resize message"; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void handle_vsh(std::optional<uint32_t> o_env_id, std::optional<uint32_t> o_cid, | 
|  | std::optional<uint32_t> o_port, async::Loop* loop, sys::ComponentContext* context) { | 
|  | uint32_t env_id, cid, port = o_port.value_or(vsh::kVshPort); | 
|  |  | 
|  | fuchsia::virtualization::ManagerSyncPtr manager; | 
|  | context->svc()->Connect(manager.NewRequest()); | 
|  | std::vector<fuchsia::virtualization::EnvironmentInfo> env_infos; | 
|  | manager->List(&env_infos); | 
|  | if (env_infos.size() == 0) { | 
|  | FX_LOGS(ERROR) << "Unable to find any environments."; | 
|  | return; | 
|  | } | 
|  | env_id = o_env_id.value_or(env_infos[0].id); | 
|  |  | 
|  | fuchsia::virtualization::RealmSyncPtr realm; | 
|  | manager->Connect(env_id, realm.NewRequest()); | 
|  | std::vector<fuchsia::virtualization::InstanceInfo> instances; | 
|  | realm->ListInstances(&instances); | 
|  | if (instances.size() == 0) { | 
|  | FX_LOGS(ERROR) << "Unable to find any instances in environment " << env_id; | 
|  | return; | 
|  | } | 
|  | cid = o_cid.value_or(instances[0].cid); | 
|  |  | 
|  | // Verify the environment and instance specified exist | 
|  | if (std::find_if(env_infos.begin(), env_infos.end(), | 
|  | [env_id](auto ei) { return ei.id == env_id; }) == env_infos.end()) { | 
|  | FX_LOGS(ERROR) << "No existing environment with id " << env_id; | 
|  | return; | 
|  | } | 
|  | if (std::find_if(instances.begin(), instances.end(), [cid](auto in) { return in.cid == cid; }) == | 
|  | instances.end()) { | 
|  | FX_LOGS(ERROR) << "No existing instances in env " << env_id << " with cid " << cid; | 
|  | return; | 
|  | } | 
|  |  | 
|  | fuchsia::virtualization::HostVsockEndpointSyncPtr vsock_endpoint; | 
|  | realm->GetHostVsockEndpoint(vsock_endpoint.NewRequest()); | 
|  |  | 
|  | // Open a socket to the guest's vsock port where vshd should be listening | 
|  | zx::socket socket, remote_socket; | 
|  | zx_status_t status = zx::socket::create(ZX_SOCKET_STREAM, &socket, &remote_socket); | 
|  | if (status != ZX_OK) { | 
|  | FX_LOGS(ERROR) << "Failed to create socket: " << zx_status_get_string(status); | 
|  | return; | 
|  | } | 
|  | vsock_endpoint->Connect(cid, port, std::move(remote_socket), &status); | 
|  | if (status != ZX_OK) { | 
|  | FX_LOGS(ERROR) << "Failed to connect: " << zx_status_get_string(status); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Now |socket| is a zircon socket plumbed to a port on the guest's vsock | 
|  | // interface. The vshd service is hopefully on the other end of this pipe. | 
|  | // We communicate with the service via protobuf messages. | 
|  | if (!init_shell(socket)) { | 
|  | FX_LOGS(ERROR) << "vsh SetupConnection failed."; | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Directly inject some helper functions for connecting to container. | 
|  | // This sleep below is to give bash some time to start after being `exec`d. | 
|  | // Otherwise the input will be duplicated in the output stream. | 
|  | usleep(100'000); | 
|  | vm_tools::vsh::GuestMessage msg_out; | 
|  | msg_out.mutable_data_message()->set_stream(vm_tools::vsh::STDIN_STREAM); | 
|  | msg_out.mutable_data_message()->set_data( | 
|  | "function stretch() { lxc exec stretch -- login -f machina ; } \n\n"); | 
|  | if (!vsh::SendMessage(socket, msg_out)) { | 
|  | FX_LOGS(WARNING) << "Failed to inject helper function"; | 
|  | } | 
|  |  | 
|  | // Set up the I/O loops | 
|  | ConsoleIn i(loop, zx::unowned_socket(socket)); | 
|  | ConsoleOut o(loop, zx::unowned_socket(socket)); | 
|  |  | 
|  | bool success = true; | 
|  | if (!i.Start()) { | 
|  | FX_LOGS(ERROR) << "Problem starting ConsoleIn loop"; | 
|  | success = false; | 
|  | } | 
|  | if (!o.Start()) { | 
|  | FX_LOGS(ERROR) << "Problem starting ConsoleOut loop"; | 
|  | success = false; | 
|  | } | 
|  |  | 
|  | if (success) { | 
|  | loop->Run(); | 
|  | } | 
|  |  | 
|  | reset_tty(); | 
|  | } |