[zircon] Add CDC Ethernet tests to the USB Virtual Bus test suite

This patch adds CDC Ethernet tests to the USB Virtual Bus test suite.

It also fixes a bug where a VMO can be accessed by a MAC driver
after the Ethernet (core) driver is unbound prior to its parent.

ZX-3812
FLK-152

Test: runtests -t usb-virtual-bus-test
Change-Id: Ib569136be8d85d16d3610c3c24f3494b3cc7909e
diff --git a/zircon/system/utest/usb-virtual-bus/BUILD.gn b/zircon/system/utest/usb-virtual-bus/BUILD.gn
index 9131ab3..24d5263 100644
--- a/zircon/system/utest/usb-virtual-bus/BUILD.gn
+++ b/zircon/system/utest/usb-virtual-bus/BUILD.gn
@@ -9,6 +9,7 @@
   ]
   deps = [
     "$zx/system/fidl/fuchsia-hardware-block:c",
+    "$zx/system/fidl/fuchsia-hardware-ethernet:c",
     "$zx/system/fidl/fuchsia-hardware-nand:c",
     "$zx/system/fidl/fuchsia-hardware-usb-peripheral:c",
     "$zx/system/fidl/fuchsia-hardware-usb-peripheral-block:c",
@@ -24,6 +25,7 @@
     "$zx/system/ulib/fbl",
     "$zx/system/ulib/fdio",
     "$zx/system/ulib/fidl-async",
+    "$zx/system/ulib/fzl",
     "$zx/system/ulib/launchpad",
     "$zx/system/ulib/ramdevice-client",
     "$zx/system/ulib/sync",
diff --git a/zircon/system/utest/usb-virtual-bus/usb-virtual-bus.cpp b/zircon/system/utest/usb-virtual-bus/usb-virtual-bus.cpp
index 597278db..9a8b76c 100644
--- a/zircon/system/utest/usb-virtual-bus/usb-virtual-bus.cpp
+++ b/zircon/system/utest/usb-virtual-bus/usb-virtual-bus.cpp
@@ -4,6 +4,7 @@
 
 #include <blktest/blktest.h>
 #include <block-client/client.h>
+#include <cstring>
 #include <ddk/platform-defs.h>
 #include <dirent.h>
 #include <endian.h>
@@ -12,6 +13,7 @@
 #include <fbl/string.h>
 #include <fbl/unique_ptr.h>
 #include <fcntl.h>
+#include <fuchsia/hardware/ethernet/c/fidl.h>
 #include <fuchsia/hardware/usb/peripheral/block/c/fidl.h>
 #include <fuchsia/hardware/usb/peripheral/c/fidl.h>
 #include <fuchsia/usb/virtualbus/c/fidl.h>
@@ -25,9 +27,14 @@
 #include <lib/fdio/unsafe.h>
 #include <lib/fdio/watcher.h>
 #include <lib/fidl-async/bind.h>
+#include <lib/fzl/vmo-mapper.h>
+#include <lib/zx/fifo.h>
+#include <lib/zx/vmo.h>
 #include <sys/stat.h>
 #include <unistd.h>
+#include <zircon/device/ethernet.h>
 #include <zircon/hw/usb.h>
+#include <zircon/hw/usb/cdc.h>
 #include <zircon/processargs.h>
 #include <zircon/syscalls.h>
 #include <zxtest/zxtest.h>
@@ -105,6 +112,61 @@
 public:
     static zx_status_t create(USBVirtualBus* bus) { return ZX_OK; }
 
+    // Initialize CDC. Asserts on failure.
+    void InitCDC(fbl::String* devpath, fbl::String* devpath1) {
+        fuchsia_hardware_usb_peripheral_DeviceDescriptor device_desc = {};
+        device_desc.bcdUSB = htole16(0x0200);
+        device_desc.bDeviceClass = 0;
+        device_desc.bDeviceSubClass = 0;
+        device_desc.bDeviceProtocol = 0;
+        device_desc.bMaxPacketSize0 = 64;
+        //   idVendor and idProduct are filled in later
+        device_desc.bcdDevice = htole16(0x0100);
+        // iManufacturer; iProduct and iSerialNumber are filled in later
+        device_desc.bNumConfigurations = 1;
+        ASSERT_EQ(ZX_OK, AllocateString(peripheral_, "Google", &device_desc.iManufacturer));
+        ASSERT_EQ(ZX_OK, AllocateString(peripheral_, "CDC Ethernet", &device_desc.iProduct));
+        ASSERT_EQ(ZX_OK, AllocateString(peripheral_, "ebfd5ad49d2a", &device_desc.iSerialNumber));
+        device_desc.idVendor = htole16(0x18D1);
+        device_desc.idProduct = htole16(0xA020);
+        ASSERT_EQ(ZX_OK, FidlCall(fuchsia_hardware_usb_peripheral_DeviceSetDeviceDescriptor,
+                                  peripheral_.get(), &device_desc));
+        fuchsia_hardware_usb_peripheral_FunctionDescriptor ums_function_desc = {
+            .interface_class = USB_CLASS_COMM,
+            .interface_subclass = USB_CDC_SUBCLASS_ETHERNET,
+            .interface_protocol = 0,
+        };
+        ASSERT_EQ(ZX_OK, FidlCall(fuchsia_hardware_usb_peripheral_DeviceAddFunction,
+                                  peripheral_.get(), &ums_function_desc));
+        zx::channel handles[2];
+        ASSERT_EQ(ZX_OK, zx::channel::create(0, handles, handles + 1));
+        ASSERT_EQ(ZX_OK, fuchsia_hardware_usb_peripheral_DeviceSetStateChangeListener(
+                             peripheral_.get(), handles[1].get()));
+        async_loop_config_t config = {};
+        async::Loop loop(&config);
+        DispatchContext context = {};
+        context.loop = &loop;
+        fuchsia_hardware_usb_peripheral_Events_ops ops;
+        ops.FunctionRegistered = DispatchStateChange;
+        async_dispatcher_t* dispatcher = loop.dispatcher();
+        fidl_bind(dispatcher, handles[0].get(), dispatch_wrapper, &context, &ops);
+        ASSERT_EQ(ZX_OK,
+                  FidlCall(fuchsia_hardware_usb_peripheral_DeviceBindFunctions, peripheral_.get()));
+        loop.Run();
+        ASSERT_TRUE(context.state_changed);
+        ASSERT_EQ(ZX_OK, FidlCall(fuchsia_usb_virtualbus_BusConnect, virtual_bus_handle_.get()));
+        fbl::unique_fd fd(openat(devmgr_.devfs_root().get(), "class/ethernet", O_RDONLY));
+        while (fdio_watch_directory(fd.get(), WaitForAnyFile, ZX_TIME_INFINITE, devpath) !=
+               ZX_ERR_STOP) {
+        }
+        *devpath = fbl::String::Concat({fbl::String("class/ethernet/"), *devpath});
+        const char* name2 = "001";
+        while (fdio_watch_directory(fd.get(), WaitForFile, ZX_TIME_INFINITE,
+                                    const_cast<char*>(name2)) != ZX_ERR_STOP) {
+        }
+        *devpath1 = "class/ethernet/001";
+    }
+
     // Initialize UMS. Asserts on failure.
     void InitUMS(fbl::String* devpath) {
         fuchsia_hardware_usb_peripheral_DeviceDescriptor device_desc = {};
@@ -161,6 +223,7 @@
     USBVirtualBus() {
         zx::channel chn;
         args_.disable_block_watcher = true;
+        args_.disable_netsvc = true;
         args_.driver_search_paths.push_back("/boot/driver");
         args_.driver_search_paths.push_back("/boot/driver/test");
         board_test::DeviceEntry dev = {};
@@ -314,6 +377,84 @@
     int root_fd_;
 };
 
+class CDCDeviceController {
+public:
+    explicit CDCDeviceController(zx::unowned_channel peripheral, zx::unowned_channel bus,
+                                 int root_fd)
+        : peripheral_(peripheral), bus_(bus), root_fd_(root_fd) {}
+
+    zx_status_t Disconnect() {
+        zx_status_t status =
+            FidlCall(fuchsia_hardware_usb_peripheral_DeviceClearFunctions, peripheral_->get());
+        if (status != ZX_OK) {
+            return status;
+        }
+        status = FidlCall(fuchsia_usb_virtualbus_BusDisconnect, bus_->get());
+        if (status != ZX_OK) {
+            return status;
+        }
+
+        return ZX_OK;
+    }
+
+    zx_status_t Connect() {
+        fuchsia_hardware_usb_peripheral_FunctionDescriptor ums_function_desc = {
+            .interface_class = USB_CLASS_COMM,
+            .interface_subclass = USB_CDC_SUBCLASS_ETHERNET,
+            .interface_protocol = 0,
+        };
+        zx_status_t status = FidlCall(fuchsia_hardware_usb_peripheral_DeviceAddFunction,
+                                      peripheral_->get(), &ums_function_desc);
+        if (status != ZX_OK) {
+            return status;
+        }
+        zx_handle_t handles[2];
+        status = zx_channel_create(0, handles, handles + 1);
+        if (status != ZX_OK) {
+            return status;
+        }
+        status = fuchsia_hardware_usb_peripheral_DeviceSetStateChangeListener(peripheral_->get(),
+                                                                              handles[1]);
+        if (status != ZX_OK) {
+            return status;
+        }
+        async_loop_config_t config = {};
+        async::Loop loop(&config);
+        DispatchContext context = {};
+        context.loop = &loop;
+        fuchsia_hardware_usb_peripheral_Events_ops ops;
+        ops.FunctionRegistered = DispatchStateChange;
+        async_dispatcher_t* dispatcher = loop.dispatcher();
+        fidl_bind(dispatcher, handles[0], dispatch_wrapper, &context, &ops);
+        status = loop.StartThread("async-thread");
+        if (status != ZX_OK) {
+            return status;
+        }
+        status = FidlCall(fuchsia_hardware_usb_peripheral_DeviceBindFunctions, peripheral_->get());
+        if (status != ZX_OK) {
+            return status;
+        }
+        loop.JoinThreads();
+        fbl::String devpath;
+        while (fdio_watch_directory(openat(root_fd_, "class/ethernet", O_RDONLY), WaitForAnyFile,
+                                    ZX_TIME_INFINITE, &devpath) != ZX_ERR_STOP)
+            ;
+        if (!context.state_changed) {
+            return ZX_ERR_INTERNAL;
+        }
+        if (status != ZX_OK) {
+            return status;
+        }
+        status = FidlCall(fuchsia_usb_virtualbus_BusConnect, bus_->get());
+        return ZX_OK;
+    }
+
+private:
+    zx::unowned_channel peripheral_;
+    zx::unowned_channel bus_;
+    int root_fd_;
+};
+
 class UmsTest : public zxtest::Test {
 public:
     void SetUp() override;
@@ -321,9 +462,10 @@
 
 protected:
     USBVirtualBus bus_;
-    fbl::String devpath_;
+    fbl::String peripheral_path_;
     zx::unowned_channel peripheral_;
     zx::unowned_channel virtual_bus_handle_;
+
     fbl::String GetTestdevPath() {
         // Open the block device
         // Special case for bad block mode. Need to enumerate the singleton block device.
@@ -360,8 +502,68 @@
     fbl::String last_known_devpath_;
 };
 
+class CDCTest : public zxtest::Test {
+public:
+    void SetUp() override;
+    void TearDown() override;
+
+protected:
+    USBVirtualBus bus_;
+    fbl::String peripheral_path_;
+    fbl::String host_path_;
+    zx::unowned_channel peripheral_;
+    zx::unowned_channel virtual_bus_handle_;
+    void GetTestdevPath() {
+        // Open the Ethernet device
+        peripheral_path_ = "";
+        host_path_ = "";
+        while (true) {
+            fbl::unique_fd fd(openat(bus_.GetRootFd(), "class/ethernet", O_RDONLY));
+            DIR* dir_handle = fdopendir(fd.get());
+            fbl::AutoCall release_dir([=]() { closedir(dir_handle); });
+            bool found = false;
+            for (dirent* ent = readdir(dir_handle); ent; ent = readdir(dir_handle)) {
+                if (strcmp(ent->d_name, ".") && strcmp(ent->d_name, "..")) {
+                    peripheral_path_ = fbl::String::Concat(
+                        {fbl::String("class/ethernet/"), fbl::String(ent->d_name)});
+                    found = true;
+                    break;
+                }
+            }
+            if (found) {
+                break;
+            }
+        }
+        while (true) {
+            fbl::unique_fd fd(openat(bus_.GetRootFd(), "class/ethernet", O_RDONLY));
+            DIR* dir_handle = fdopendir(fd.get());
+            fbl::AutoCall release_dir([=]() { closedir(dir_handle); });
+            for (dirent* ent = readdir(dir_handle); ent; ent = readdir(dir_handle)) {
+                if (strcmp(ent->d_name, ".") && strcmp(ent->d_name, "..") &&
+                    (strcmp(ent->d_name, peripheral_path_.c_str()))) {
+                    host_path_ = fbl::String::Concat(
+                        {fbl::String("class/ethernet/"), fbl::String(ent->d_name)});
+                    return;
+                }
+            }
+        }
+    }
+
+    // Waits for the Ethernet device to be removed
+    // TODO (ZX-3385, ZX-3586) -- Use something better
+    // than a busy loop.
+    void WaitForRemove() {
+        struct stat dirinfo;
+        while (!stat(last_known_devpath_.c_str(), &dirinfo))
+            ;
+    }
+
+private:
+    fbl::String last_known_devpath_;
+};
+
 void UmsTest::SetUp() {
-    ASSERT_NO_FATAL_FAILURES(bus_.InitUMS(&devpath_));
+    ASSERT_NO_FATAL_FAILURES(bus_.InitUMS(&peripheral_path_));
     bus_.GetHandles(&peripheral_, &virtual_bus_handle_);
 }
 
@@ -371,6 +573,16 @@
     ASSERT_EQ(ZX_OK, FidlCall(fuchsia_usb_virtualbus_BusDisable, virtual_bus_handle_->get()));
 }
 
+void CDCTest::SetUp() {
+    ASSERT_NO_FATAL_FAILURES(bus_.InitCDC(&peripheral_path_, &host_path_));
+    bus_.GetHandles(&peripheral_, &virtual_bus_handle_);
+}
+
+void CDCTest::TearDown() {
+    ASSERT_EQ(ZX_OK,
+              FidlCall(fuchsia_hardware_usb_peripheral_DeviceClearFunctions, peripheral_->get()));
+    ASSERT_EQ(ZX_OK, FidlCall(fuchsia_usb_virtualbus_BusDisable, virtual_bus_handle_->get()));
+}
 TEST_F(UmsTest, ReconnectTest) {
     // Disconnect and re-connect the block device 50 times as a sanity check
     // for race conditions and deadlocks.
@@ -399,14 +611,14 @@
     block_info_t info;
     __UNUSED ssize_t rc = ioctl_block_get_info(fd.get(), &info);
     uint32_t blk_size = info.block_size;
-    fbl::unique_ptr<uint8_t[]> write_buffer(new uint8_t[blk_size]);
-    fbl::unique_ptr<uint8_t[]> read_buffer(new uint8_t[blk_size]);
+    std::unique_ptr<uint8_t[]> write_buffer(new uint8_t[blk_size]);
+    std::unique_ptr<uint8_t[]> read_buffer(new uint8_t[blk_size]);
     ASSERT_EQ(blk_size, static_cast<uint64_t>(read(fd.get(), read_buffer.get(), blk_size)));
     close(fd.release());
     fd = fbl::unique_fd(openat(bus_.GetRootFd(), GetTestdevPath().c_str(), O_RDWR));
     // Create a pattern to write to the block device
     for (size_t i = 0; i < blk_size; i++) {
-        write_buffer.get()[i] = static_cast<unsigned char>(i);
+        write_buffer.get()[i] = static_cast<uint8_t>(i);
     }
     // Write the data to the block device
     ASSERT_EQ(blk_size, static_cast<uint64_t>(write(fd.get(), write_buffer.get(), blk_size)));
@@ -434,10 +646,10 @@
     __UNUSED ssize_t rc = ioctl_block_get_info(fd.get(), &info);
     uint32_t blk_size = info.block_size;
     // Allocate our buffer
-    fbl::unique_ptr<uint8_t[]> write_buffer(new uint8_t[blk_size]);
+    std::unique_ptr<uint8_t[]> write_buffer(new uint8_t[blk_size]);
     // Generate and write a pattern to the block device
     for (size_t i = 0; i < blk_size; i++) {
-        write_buffer.get()[i] = static_cast<unsigned char>(i);
+        write_buffer.get()[i] = static_cast<uint8_t>(i);
     }
     ASSERT_EQ(blk_size, static_cast<uint64_t>(write(fd.get(), write_buffer.get(), blk_size)));
     memset(write_buffer.get(), 0, blk_size);
@@ -450,7 +662,7 @@
     // since writeback caching was disabled.
     ASSERT_EQ(blk_size, static_cast<uint64_t>(read(fd.get(), write_buffer.get(), blk_size)));
     for (size_t i = 0; i < blk_size; i++) {
-        ASSERT_EQ(write_buffer.get()[i], static_cast<unsigned char>(i));
+        ASSERT_EQ(write_buffer.get()[i], static_cast<uint8_t>(i));
     }
 }
 
@@ -476,4 +688,227 @@
     EXPECT_EQ(proc_info.return_code, 0);
 }
 
+class EthernetInterface {
+public:
+    EthernetInterface(fbl::unique_fd fd) {
+        ASSERT_OK(fdio_get_service_handle(fd.get(), ethernet_handle_.reset_and_get_address()));
+
+        // Get device information
+        fuchsia_hardware_ethernet_Info info;
+        ASSERT_OK(fuchsia_hardware_ethernet_DeviceGetInfo(ethernet_handle_.get(), &info));
+        zx_status_t status1;
+        fuchsia_hardware_ethernet_Fifos fifos;
+        ASSERT_OK(
+            fuchsia_hardware_ethernet_DeviceGetFifos(ethernet_handle_.get(), &status1, &fifos));
+        ASSERT_OK(status1);
+        rx_depth_ = fifos.rx_depth;
+        tx_depth_ = fifos.tx_depth;
+        mtu_ = static_cast<uint16_t>(info.mtu);
+        // Calculate optimal size of VMO, and set up RX and TX buffers.
+        size_t optimal_vmo_size = (fifos.rx_depth * info.mtu) + (fifos.tx_depth * info.mtu);
+        uint64_t size;
+        ASSERT_OK(zx::vmo::create(optimal_vmo_size, ZX_VMO_NON_RESIZABLE, &vmo_));
+        ASSERT_OK(vmo_.get_size(&size));
+        ASSERT_OK(mapper_.Map(vmo_, 0, size, ZX_VM_FLAG_PERM_READ | ZX_VM_FLAG_PERM_WRITE));
+        xfer_region_ = static_cast<uint8_t*>(mapper_.start());
+        ASSERT_OK(FidlCall(fuchsia_hardware_ethernet_DeviceSetIOBuffer, ethernet_handle_.get(),
+                           vmo_.get()));
+        ASSERT_OK(FidlCall(fuchsia_hardware_ethernet_DeviceStart, ethernet_handle_.get()));
+        tx_.reset(fifos.tx);
+        rx_.reset(fifos.rx);
+        // Give all RX entries to the Ethernet driver
+        size_t count;
+        rx_entries_ = std::make_unique<eth_fifo_entry_t[]>(rx_depth_);
+        for (uint32_t i = 0; i < rx_depth_; i++) {
+            rx_entries_[i].offset = i * info.mtu;
+            rx_entries_[i].length = static_cast<uint16_t>(info.mtu);
+            rx_entries_[i].flags = 0;
+            rx_entries_[i].cookie = 0;
+        }
+        ASSERT_OK(rx_.write(sizeof(eth_fifo_entry_t), rx_entries_.get(), rx_depth_, &count));
+        ASSERT_EQ(count, rx_depth_);
+    }
+
+    // Receives data from the EthernetInterface
+    // Returns an array of eth_fifo_entry_t which are owned by this EthernetInterface.
+    // The number of entries received is stored in length.
+    // If this function returns something other than ZX_OK, the values of *output and *length
+    // are undefined.
+    zx_status_t Receive(eth_fifo_entry_t** output, size_t* length) {
+        zx_signals_t pending = 0;
+        rx_.wait_one(ZX_FIFO_READABLE | ZX_FIFO_PEER_CLOSED, zx::time::infinite(), &pending);
+        zx_status_t status =
+            rx_.read(sizeof(eth_fifo_entry_t), rx_entries_.get(), rx_depth_, length);
+        if (status != ZX_OK) {
+            return status;
+        }
+        *output = rx_entries_.get();
+        return ZX_OK;
+    }
+
+    zx_status_t Send(const eth_fifo_entry_t* data, size_t* length) {
+        return tx_.write(sizeof(eth_fifo_entry_t), data, *length, length);
+    }
+
+    std::unique_ptr<eth_fifo_entry_t[]> AllocateTXEntries() {
+        auto entries = std::make_unique<eth_fifo_entry_t[]>(tx_depth_);
+        for (uint32_t i = 0; i < tx_depth_; i++) {
+            entries[i].flags = 0;
+            entries[i].length = mtu_;
+            entries[i].offset = static_cast<uint32_t>((i * mtu_) + (rx_depth_ * mtu_));
+            entries[i].cookie = 1;
+        }
+        return entries;
+    }
+
+    size_t tx_depth() { return tx_depth_; }
+
+    uint8_t* xfer_region() { return xfer_region_; }
+
+private:
+    fzl::VmoMapper mapper_;
+    uint8_t* xfer_region_;
+    zx::channel ethernet_handle_;
+    zx::fifo rx_;
+    zx::fifo tx_;
+    zx::vmo vmo_;
+    size_t rx_depth_;
+    size_t tx_depth_;
+    uint16_t mtu_;
+    std::unique_ptr<eth_fifo_entry_t[]> rx_entries_;
+    DISALLOW_COPY_ASSIGN_AND_MOVE(EthernetInterface);
+};
+
+TEST_F(CDCTest, ReconnectTest) {
+    // Disconnect and re-connect the Ethernet interface 50 times as a sanity check
+    // for race conditions and deadlocks.
+    // If the test freezes; or something crashes at this point, it is likely
+    // a regression in a driver (not a test flake).
+    CDCDeviceController controller(zx::unowned_channel(peripheral_),
+                                   zx::unowned_channel(virtual_bus_handle_), bus_.GetRootFd());
+    for (size_t i = 0; i < 50; i++) {
+        ASSERT_OK(controller.Disconnect());
+        WaitForRemove();
+        ASSERT_OK(controller.Connect());
+        GetTestdevPath();
+        EthernetInterface peripheral_ethernet(
+            fbl::unique_fd(openat(bus_.GetRootFd(), peripheral_path_.c_str(), O_RDWR)));
+        EthernetInterface host_ethernet(
+            fbl::unique_fd(openat(bus_.GetRootFd(), host_path_.c_str(), O_RDWR)));
+        ASSERT_NO_FATAL_FAILURES();
+        auto tx_buffer = peripheral_ethernet.AllocateTXEntries();
+        size_t length = peripheral_ethernet.tx_depth();
+        ASSERT_OK(peripheral_ethernet.Send(tx_buffer.get(), &length));
+        ASSERT_EQ(length, peripheral_ethernet.tx_depth());
+    }
+}
+
+TEST_F(CDCTest, EthernetDoesNotCrashOnUnbindWithPendingTransmitsOnPeripheralSide) {
+    EthernetInterface peripheral_ethernet(
+        fbl::unique_fd(openat(bus_.GetRootFd(), peripheral_path_.c_str(), O_RDWR)));
+    EthernetInterface host_ethernet(
+        fbl::unique_fd(openat(bus_.GetRootFd(), host_path_.c_str(), O_RDWR)));
+    ASSERT_NO_FATAL_FAILURES();
+    auto tx_buffer = peripheral_ethernet.AllocateTXEntries();
+    size_t length = peripheral_ethernet.tx_depth();
+    ASSERT_OK(peripheral_ethernet.Send(tx_buffer.get(), &length));
+    ASSERT_EQ(length, peripheral_ethernet.tx_depth());
+}
+
+TEST_F(CDCTest, EthernetDoesNotCrashOnUnbindWithPendingTransmitsOnHostSide) {
+    EthernetInterface host_ethernet(
+        fbl::unique_fd(openat(bus_.GetRootFd(), host_path_.c_str(), O_RDWR)));
+    ASSERT_NO_FATAL_FAILURES();
+    auto tx = host_ethernet.AllocateTXEntries();
+    size_t length = host_ethernet.tx_depth();
+    ASSERT_OK(host_ethernet.Send(tx.get(), &length));
+    ASSERT_EQ(length, host_ethernet.tx_depth());
+}
+
+TEST_F(CDCTest, PeripheralTransmitsToHost) {
+    EthernetInterface peripheral_ethernet(
+        fbl::unique_fd(openat(bus_.GetRootFd(), peripheral_path_.c_str(), O_RDWR)));
+    EthernetInterface host_ethernet(
+        fbl::unique_fd(openat(bus_.GetRootFd(), host_path_.c_str(), O_RDWR)));
+    ASSERT_NO_FATAL_FAILURES();
+    auto tx_buffer = peripheral_ethernet.AllocateTXEntries();
+    for (size_t i = 0; i < peripheral_ethernet.tx_depth(); i++) {
+        auto info = tx_buffer[i];
+        memcpy(peripheral_ethernet.xfer_region() + info.offset, &i, sizeof(i));
+        for (size_t c = sizeof(i); c < info.length; c++) {
+            peripheral_ethernet.xfer_region()[c + info.offset] =
+                c == 16 ? 255 : static_cast<uint8_t>(c + i);
+        }
+    }
+    size_t length = peripheral_ethernet.tx_depth();
+    ASSERT_OK(peripheral_ethernet.Send(tx_buffer.get(), &length));
+    ASSERT_EQ(length, peripheral_ethernet.tx_depth());
+    auto completions = std::make_unique<bool[]>(length);
+    memset(completions.get(), 0, sizeof(bool) * length);
+    size_t packets_received = 0;
+    while (packets_received < length) {
+        eth_fifo_entry_t* entries = nullptr;
+        size_t len;
+        ASSERT_OK(host_ethernet.Receive(&entries, &len));
+        if (host_ethernet.xfer_region()[entries[0].offset + 16] == 255) {
+            for (size_t i = 0; i < len; i++) {
+                eth_fifo_entry_t entry = entries[i];
+                size_t idx;
+                memcpy(&idx, host_ethernet.xfer_region() + entry.offset, sizeof(idx));
+                for (size_t c = sizeof(size_t); c < entry.length; c++) {
+                    ASSERT_EQ(host_ethernet.xfer_region()[entry.offset + c],
+                              c == 16 ? 255 : static_cast<uint8_t>(c + idx));
+                }
+                if (!completions[idx]) {
+                    packets_received++;
+                    completions[idx] = true;
+                }
+            }
+        }
+    }
+}
+
+TEST_F(CDCTest, HostTransmitsToPeripheral) {
+    EthernetInterface peripheral_ethernet(
+        fbl::unique_fd(openat(bus_.GetRootFd(), peripheral_path_.c_str(), O_RDWR)));
+    EthernetInterface host_ethernet(
+        fbl::unique_fd(openat(bus_.GetRootFd(), host_path_.c_str(), O_RDWR)));
+    ASSERT_NO_FATAL_FAILURES();
+    auto tx_buffer = host_ethernet.AllocateTXEntries();
+    for (size_t i = 0; i < host_ethernet.tx_depth(); i++) {
+        auto info = tx_buffer[i];
+        memcpy(host_ethernet.xfer_region() + info.offset, &i, sizeof(i));
+        for (size_t c = sizeof(i); c < info.length; c++) {
+            host_ethernet.xfer_region()[c + info.offset] =
+                c == 16 ? 255 : static_cast<uint8_t>(c + i);
+        }
+    }
+    size_t length = host_ethernet.tx_depth();
+    ASSERT_OK(host_ethernet.Send(tx_buffer.get(), &length));
+    ASSERT_EQ(length, host_ethernet.tx_depth());
+    std::unique_ptr<bool[]> completions = std::make_unique<bool[]>(length);
+    memset(completions.get(), 0, sizeof(bool) * length);
+    size_t packets_received = 0;
+    while (packets_received < length) {
+        eth_fifo_entry_t* entries = nullptr;
+        size_t len;
+        ASSERT_OK(peripheral_ethernet.Receive(&entries, &len));
+        if (peripheral_ethernet.xfer_region()[entries[0].offset + 16] == 255) {
+            for (size_t i = 0; i < len; i++) {
+                eth_fifo_entry_t entry = entries[i];
+                size_t idx;
+                memcpy(&idx, peripheral_ethernet.xfer_region() + entry.offset, sizeof(idx));
+                for (size_t c = sizeof(size_t); c < entry.length; c++) {
+                    ASSERT_EQ(peripheral_ethernet.xfer_region()[entry.offset + c],
+                              c == 16 ? 255 : static_cast<uint8_t>(c + idx));
+                }
+                if (!completions[idx]) {
+                    packets_received++;
+                    completions[idx] = true;
+                }
+            }
+        }
+    }
+}
+
 } // namespace