diff --git a/zircon/system/core/devmgr/devhost/BUILD.gn b/zircon/system/core/devmgr/devhost/BUILD.gn
index d83b5cc5759cbfe19ea87ae8c153d91059630edf..cf3143a98a84c1ddcd01595be8cdbc677ed83137 100644
--- a/zircon/system/core/devmgr/devhost/BUILD.gn
+++ b/zircon/system/core/devmgr/devhost/BUILD.gn
@@ -11,24 +11,23 @@ executable("devhost") {
   ]
 }
 
-# "$zx/system/ulib/driver" is an alias for this.
-library("driver") {
-  sdk = "shared"
-  sdk_headers = []
-  shared = true
-  static = false
+source_set("common") {
+  visibility = [ ":*" ]
+
   sources = [
     "api.cpp",
     "composite-device.cpp",
+    "connection-destroyer.cpp",
     "core.cpp",
     "devhost.cpp",
+    "proxy-iostate.cpp",
     "rpc-server.cpp",
     "scheduler_profile.cpp",
     "tracing.cpp",
     "zx-device.cpp",
   ]
-  configs += [ "$zx/public/gn/config:visibility_hidden" ]
-  deps = [
+
+  public_deps = [
     "$zx/system/banjo/ddk.protocol.composite",
     "$zx/system/fidl/fuchsia-device:c",
     "$zx/system/fidl/fuchsia-device-manager:c",
@@ -53,6 +52,21 @@ library("driver") {
     "../shared:env",
   ]
   configs += [ "$zx/public/gn/config:static-libc++" ]
+}
+
+# "$zx/system/ulib/driver" is an alias for this.
+library("driver") {
+  sdk = "shared"
+  sdk_headers = []
+  shared = true
+  static = false
+  configs += [ "$zx/public/gn/config:visibility_hidden" ]
+
+  sources = []
+
+  deps = [
+    ":common",
+  ]
 
   # Since the tracing support is brought in via an archive, we need explicit
   # references to ensure everything is present.
@@ -60,3 +74,15 @@ library("driver") {
 
   assert_no_deps = [ "$zx/system/ulib/trace-engine:trace-engine.shared" ]
 }
+
+test("devhost-test") {
+  test_group = "ddk"
+  sources = [
+    "proxy-iostate-test.cpp",
+  ]
+  deps = [
+    ":common",
+    "$zx/system/ulib/zxtest",
+    "../shared:env",
+  ]
+}
diff --git a/zircon/system/core/devmgr/devhost/connection-destroyer.cpp b/zircon/system/core/devmgr/devhost/connection-destroyer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a834ce570aebf45c39f79934d1ddf76f1174b697
--- /dev/null
+++ b/zircon/system/core/devmgr/devhost/connection-destroyer.cpp
@@ -0,0 +1,54 @@
+// 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 "connection-destroyer.h"
+
+#include <inttypes.h>
+#include "../shared/log.h"
+#include "devhost.h"
+#include "proxy-iostate.h"
+
+namespace devmgr {
+
+zx_status_t ConnectionDestroyer::QueueProxyConnection(async_dispatcher_t* dispatcher,
+                                                      ProxyIostate* conn) {
+    zx_packet_user_t pkt = {};
+    pkt.u64[0] = static_cast<uint64_t>(Type::Proxy);
+    pkt.u64[1] = reinterpret_cast<uintptr_t>(conn);
+    return receiver_.QueuePacket(dispatcher, &pkt);
+}
+
+zx_status_t ConnectionDestroyer::QueueDeviceControllerConnection(
+        async_dispatcher_t* dispatcher, DeviceControllerConnection* conn) {
+    zx_packet_user_t pkt = {};
+    pkt.u64[0] = static_cast<uint64_t>(Type::DeviceController);
+    pkt.u64[1] = reinterpret_cast<uintptr_t>(conn);
+    return receiver_.QueuePacket(dispatcher, &pkt);
+}
+
+void ConnectionDestroyer::Handler(async_dispatcher_t* dispatcher, async::Receiver* receiver,
+                                  zx_status_t status, const zx_packet_user_t* data) {
+    Type type = static_cast<Type>(data->u64[0]);
+    uintptr_t ptr = data->u64[1];
+
+    switch (type) {
+    case Type::DeviceController: {
+        auto conn = reinterpret_cast<DeviceControllerConnection*>(ptr);
+        log(TRACE, "devhost: destroying devcoord conn '%p'\n", conn);
+        delete conn;
+        break;
+    }
+    case Type::Proxy: {
+        auto conn = reinterpret_cast<ProxyIostate*>(ptr);
+        log(TRACE, "devhost: destroying proxy conn '%p'\n", conn);
+        delete conn;
+        break;
+    }
+    default:
+        ZX_ASSERT_MSG(false, "Unknown IosDestructionType %" PRIu64 "\n", data->u64[0]);
+    }
+}
+
+} // namespace devmgr
+
diff --git a/zircon/system/core/devmgr/devhost/connection-destroyer.h b/zircon/system/core/devmgr/devhost/connection-destroyer.h
new file mode 100644
index 0000000000000000000000000000000000000000..89b3fa723e89fe1c6cad7337fa0c7cf7b4fec785
--- /dev/null
+++ b/zircon/system/core/devmgr/devhost/connection-destroyer.h
@@ -0,0 +1,50 @@
+// 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.
+
+#pragma once
+
+#include <lib/async/cpp/receiver.h>
+#include <zircon/syscalls.h>
+
+namespace devmgr {
+
+struct DeviceControllerConnection;
+struct ProxyIostate;
+
+// Handles destroying Connection objects in the single-threaded DevhostAsyncLoop().
+// This allows us to prevent races between canceling waiting on the connection
+// channel and executing the connection's handler.
+class ConnectionDestroyer {
+public:
+    static ConnectionDestroyer* Get() {
+        static ConnectionDestroyer destroyer;
+        return &destroyer;
+    }
+
+    zx_status_t QueueDeviceControllerConnection(async_dispatcher_t* dispatcher,
+                                                DeviceControllerConnection* conn);
+    zx_status_t QueueProxyConnection(async_dispatcher_t* dispatcher,
+                                     ProxyIostate* conn);
+
+private:
+    ConnectionDestroyer() = default;
+
+    ConnectionDestroyer(const ConnectionDestroyer&) = delete;
+    ConnectionDestroyer& operator=(const ConnectionDestroyer&) = delete;
+
+    ConnectionDestroyer(ConnectionDestroyer&&) = delete;
+    ConnectionDestroyer& operator=(ConnectionDestroyer&&) = delete;
+
+    static void Handler(async_dispatcher_t* dispatcher, async::Receiver* receiver,
+                        zx_status_t status, const zx_packet_user_t* data);
+
+    enum class Type {
+        DeviceController,
+        Proxy,
+    };
+
+    async::Receiver receiver_{ConnectionDestroyer::Handler};
+};
+
+} // namespace devmgr
diff --git a/zircon/system/core/devmgr/devhost/core.cpp b/zircon/system/core/devmgr/devhost/core.cpp
index e29908a5486f0e82185e0c857d465b801c7bdaf3..2cd57ab373cb2ec2872ffbe8f2682cd89c7378a6 100644
--- a/zircon/system/core/devmgr/devhost/core.cpp
+++ b/zircon/system/core/devmgr/devhost/core.cpp
@@ -162,7 +162,20 @@ static zx_protocol_device_t device_invalid_ops = []() {
 #define DEAD_DEVICE_MAX 7
 
 void devhost_device_destroy(zx_device_t* dev) REQ_DM_LOCK {
-    static fbl::DoublyLinkedList<zx_device*, zx_device::Node> dead_list;
+    // Wrap the deferred-deletion list in a struct, so we can give it a proper
+    // dtor.  Otherwise, this causes the binary to crash on exit due to an
+    // is_empty assert in fbl::DoublyLinkedList.  This was particularly a
+    // problem for unit tests.
+    struct DeadList {
+        ~DeadList() {
+            while (!devices.is_empty()) {
+                delete devices.pop_front();
+            }
+        }
+        fbl::DoublyLinkedList<zx_device*, zx_device::Node> devices;
+    };
+
+    static DeadList dead_list;
     static unsigned dead_count = 0;
 
     // ensure any ops will be fatal
@@ -187,10 +200,10 @@ void devhost_device_destroy(zx_device_t* dev) REQ_DM_LOCK {
     // Defer destruction to help catch use-after-free and also
     // so the compiler can't (easily) optimize away the poisoning
     // we do above.
-    dead_list.push_back(dev);
+    dead_list.devices.push_back(dev);
 
     if (dead_count == DEAD_DEVICE_MAX) {
-        zx_device_t* to_delete = dead_list.pop_front();
+        zx_device_t* to_delete = dead_list.devices.pop_front();
         delete to_delete;
     } else {
         dead_count++;
diff --git a/zircon/system/core/devmgr/devhost/devhost.cpp b/zircon/system/core/devmgr/devhost/devhost.cpp
index 97b14bb99e0648ba7de0244de7ceb59fad9d4f8a..bb0fb46d96a7fe9fb8af8fcee6d073192c3b0b07 100644
--- a/zircon/system/core/devmgr/devhost/devhost.cpp
+++ b/zircon/system/core/devmgr/devhost/devhost.cpp
@@ -46,7 +46,9 @@
 #include "../shared/fidl_txn.h"
 #include "../shared/log.h"
 #include "composite-device.h"
+#include "connection-destroyer.h"
 #include "main.h"
+#include "proxy-iostate.h"
 #include "scheduler_profile.h"
 #include "tracing.h"
 
@@ -59,26 +61,6 @@ namespace devmgr {
 
 uint32_t log_flags = LOG_ERROR | LOG_INFO;
 
-struct ProxyIostate : AsyncLoopOwnedRpcHandler<ProxyIostate> {
-    ProxyIostate() = default;
-    ~ProxyIostate();
-
-    // Creates a ProxyIostate and points |dev| at it.  The ProxyIostate is owned
-    // by the async loop, and its destruction may be requested by calling
-    // Cancel().
-    static zx_status_t Create(const fbl::RefPtr<zx_device_t>& dev, zx::channel rpc);
-
-    // Request the destruction of the proxy connection
-    void Cancel();
-
-    static void HandleRpc(fbl::unique_ptr<ProxyIostate> conn, async_dispatcher_t* dispatcher,
-                          async::WaitBase* wait, zx_status_t status,
-                          const zx_packet_signal_t* signal);
-
-    fbl::RefPtr<zx_device_t> dev;
-};
-static void proxy_ios_destroy(const fbl::RefPtr<zx_device_t>& dev);
-
 static fbl::DoublyLinkedList<fbl::RefPtr<zx_driver>> dh_drivers;
 
 // Access the devhost's async event loop
@@ -98,77 +80,6 @@ static zx_status_t SetupRootDevcoordinatorConnection(zx::channel ch) {
                                                       DevhostAsyncLoop()->dispatcher());
 }
 
-// Handles destroying Connection objects in the single-threaded DevhostAsyncLoop().
-// This allows us to prevent races between canceling waiting on the connection
-// channel and executing the connection's handler.
-class ConnectionDestroyer {
-public:
-    static ConnectionDestroyer* Get() {
-        static ConnectionDestroyer destroyer;
-        return &destroyer;
-    }
-
-    zx_status_t QueueDeviceControllerConnection(DeviceControllerConnection* conn);
-    zx_status_t QueueProxyConnection(ProxyIostate* conn);
-
-private:
-    ConnectionDestroyer() = default;
-
-    ConnectionDestroyer(const ConnectionDestroyer&) = delete;
-    ConnectionDestroyer& operator=(const ConnectionDestroyer&) = delete;
-
-    ConnectionDestroyer(ConnectionDestroyer&&) = delete;
-    ConnectionDestroyer& operator=(ConnectionDestroyer&&) = delete;
-
-    static void Handler(async_dispatcher_t* dispatcher, async::Receiver* receiver,
-                        zx_status_t status, const zx_packet_user_t* data);
-
-    enum class Type {
-        DeviceController,
-        Proxy,
-    };
-
-    async::Receiver receiver_{ConnectionDestroyer::Handler};
-};
-
-zx_status_t ConnectionDestroyer::QueueProxyConnection(ProxyIostate* conn) {
-    zx_packet_user_t pkt = {};
-    pkt.u64[0] = static_cast<uint64_t>(Type::Proxy);
-    pkt.u64[1] = reinterpret_cast<uintptr_t>(conn);
-    return receiver_.QueuePacket(DevhostAsyncLoop()->dispatcher(), &pkt);
-}
-
-zx_status_t ConnectionDestroyer::QueueDeviceControllerConnection(
-        DeviceControllerConnection* conn) {
-    zx_packet_user_t pkt = {};
-    pkt.u64[0] = static_cast<uint64_t>(Type::DeviceController);
-    pkt.u64[1] = reinterpret_cast<uintptr_t>(conn);
-    return receiver_.QueuePacket(DevhostAsyncLoop()->dispatcher(), &pkt);
-}
-
-void ConnectionDestroyer::Handler(async_dispatcher_t* dispatcher, async::Receiver* receiver,
-                                  zx_status_t status, const zx_packet_user_t* data) {
-    Type type = static_cast<Type>(data->u64[0]);
-    uintptr_t ptr = data->u64[1];
-
-    switch (type) {
-    case Type::DeviceController: {
-        auto conn = reinterpret_cast<DeviceControllerConnection*>(ptr);
-        log(TRACE, "devhost: destroying devcoord conn '%p'\n", conn);
-        delete conn;
-        break;
-    }
-    case Type::Proxy: {
-        auto conn = reinterpret_cast<ProxyIostate*>(ptr);
-        log(TRACE, "devhost: destroying proxy conn '%p'\n", conn);
-        delete conn;
-        break;
-    }
-    default:
-        ZX_ASSERT_MSG(false, "Unknown IosDestructionType %" PRIu64 "\n", data->u64[0]);
-    }
-}
-
 static const char* mkdevpath(const fbl::RefPtr<zx_device_t>& dev, char* path, size_t max) {
     if (dev == nullptr) {
         return "";
@@ -607,7 +518,7 @@ static zx_status_t fidl_ConnectProxy(void* raw_ctx, zx_handle_t raw_shadow) {
     ctx->conn->dev->ops->rxrpc(ctx->conn->dev->ctx, ZX_HANDLE_INVALID);
     // Ignore any errors in the creation for now?
     // TODO(teisenbe/kulakowski): Investigate if this is the right thing
-    ProxyIostate::Create(ctx->conn->dev, std::move(shadow));
+    ProxyIostate::Create(ctx->conn->dev, std::move(shadow), DevhostAsyncLoop()->dispatcher());
     return ZX_OK;
 }
 
@@ -824,91 +735,11 @@ void DevfsConnection::HandleRpc(fbl::unique_ptr<DevfsConnection> conn,
     log(TRACE, "devhost: destroying devfs conn %p\n", conn.get());
 }
 
-ProxyIostate::~ProxyIostate() {
-    fbl::AutoLock guard(&dev->proxy_ios_lock);
-    if (dev->proxy_ios == this) {
-        dev->proxy_ios = nullptr;
-    }
-}
-
-// Handling RPC From Proxy Devices to BusDevs
-void ProxyIostate::HandleRpc(fbl::unique_ptr<ProxyIostate> conn, async_dispatcher_t* dispatcher,
-                             async::WaitBase* wait, zx_status_t status,
-                             const zx_packet_signal_t* signal) {
-    if (status != ZX_OK) {
-        return;
-    }
-
-    if (conn->dev == nullptr) {
-        log(RPC_SDW, "proxy-rpc: stale rpc? (ios=%p)\n", conn.get());
-        // Do not re-issue the wait here
-        return;
-    }
-    if (signal->observed & ZX_CHANNEL_READABLE) {
-        log(RPC_SDW, "proxy-rpc: rpc readable (ios=%p,dev=%p)\n", conn.get(), conn->dev.get());
-        zx_status_t r = conn->dev->ops->rxrpc(conn->dev->ctx, wait->object());
-        if (r != ZX_OK) {
-            log(RPC_SDW, "proxy-rpc: rpc cb error %d (ios=%p,dev=%p)\n", r, conn.get(),
-                conn->dev.get());
-            // Let |conn| be destroyed
-            return;
-        }
-        BeginWait(std::move(conn), dispatcher);
-        return;
-    }
-    if (signal->observed & ZX_CHANNEL_PEER_CLOSED) {
-        log(RPC_SDW, "proxy-rpc: peer closed (ios=%p,dev=%p)\n", conn.get(), conn->dev.get());
-        // Let |conn| be destroyed
-        return;
-    }
-    log(ERROR, "devhost: no work? %08x\n", signal->observed);
-    BeginWait(std::move(conn), dispatcher);
-}
-
-zx_status_t ProxyIostate::Create(const fbl::RefPtr<zx_device_t>& dev, zx::channel rpc) {
-    // This must be held for the adding of the channel to the port, since the
-    // async loop may run immediately after that point.
-    fbl::AutoLock guard(&dev->proxy_ios_lock);
-
-    if (dev->proxy_ios) {
-        dev->proxy_ios->Cancel();
-        dev->proxy_ios = nullptr;
-    }
-
-    auto ios = std::make_unique<ProxyIostate>();
-    if (ios == nullptr) {
-        return ZX_ERR_NO_MEMORY;
-    }
-
-    ios->dev = dev;
-    ios->set_channel(std::move(rpc));
-
-    // |ios| is will be owned by the async loop.  |dev| holds a reference that will be
-    // cleared prior to destruction.
-    dev->proxy_ios = ios.get();
-
-    zx_status_t status = BeginWait(std::move(ios), DevhostAsyncLoop()->dispatcher());
-    if (status != ZX_OK) {
-        dev->proxy_ios = nullptr;
-        return status;
-    }
-
-    return ZX_OK;
-}
-
-// The device for which ProxyIostate is currently attached to should have
-// its proxy_ios_lock held across Cancel().
-void ProxyIostate::Cancel() {
-    // TODO(teisenbe): We should probably check the return code in case the
-    // queue was full
-    ConnectionDestroyer::Get()->QueueProxyConnection(this);
-}
-
 static void proxy_ios_destroy(const fbl::RefPtr<zx_device_t>& dev) {
     fbl::AutoLock guard(&dev->proxy_ios_lock);
 
     if (dev->proxy_ios) {
-        dev->proxy_ios->Cancel();
+        dev->proxy_ios->Cancel(DevhostAsyncLoop()->dispatcher());
     }
     dev->proxy_ios = nullptr;
 }
@@ -1144,7 +975,8 @@ zx_status_t devhost_remove(const fbl::RefPtr<zx_device_t>& dev) {
     dev->rpc = zx::unowned_channel();
 
     // queue an event to destroy the connection
-    ConnectionDestroyer::Get()->QueueDeviceControllerConnection(conn);
+    ConnectionDestroyer::Get()->QueueDeviceControllerConnection(DevhostAsyncLoop()->dispatcher(),
+                                                                conn);
 
     // shut down our proxy rpc channel if it exists
     proxy_ios_destroy(dev);
diff --git a/zircon/system/core/devmgr/devhost/devhost.h b/zircon/system/core/devmgr/devhost/devhost.h
index 74f1521686564a128ad96b5116438b3a786f9770..da85a0a84be66c415c54b0973ce961d7e942ff29 100644
--- a/zircon/system/core/devmgr/devhost/devhost.h
+++ b/zircon/system/core/devmgr/devhost/devhost.h
@@ -18,6 +18,7 @@
 #include <fbl/string.h>
 #include <fbl/unique_ptr.h>
 #include <lib/async/cpp/wait.h>
+#include <lib/async-loop/cpp/loop.h>
 #include <lib/zx/channel.h>
 #include <zircon/compiler.h>
 #include <zircon/fidl.h>
diff --git a/zircon/system/core/devmgr/devhost/proxy-iostate-test.cpp b/zircon/system/core/devmgr/devhost/proxy-iostate-test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..752ab43f5ad802364feb198cf1ac686e00eef924
--- /dev/null
+++ b/zircon/system/core/devmgr/devhost/proxy-iostate-test.cpp
@@ -0,0 +1,28 @@
+// 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 <lib/async-loop/cpp/loop.h>
+#include <zxtest/zxtest.h>
+#include "proxy-iostate.h"
+#include "zx-device.h"
+
+TEST(ProxyIostateTestCase, Creation) {
+    async::Loop loop(&kAsyncLoopConfigNoAttachToThread);
+
+    fbl::RefPtr<zx_device> dev;
+    ASSERT_OK(zx_device::Create(&dev));
+
+    zx::channel proxy_local, proxy_remote;
+    ASSERT_OK(zx::channel::create(0, &proxy_local, &proxy_remote));
+
+    ASSERT_OK(devmgr::ProxyIostate::Create(dev, std::move(proxy_remote), loop.dispatcher()));
+
+    ASSERT_OK(loop.RunUntilIdle());
+}
+
+
+int main(int argc, char** argv) {
+    return RUN_ALL_TESTS(argc, argv);
+}
+
diff --git a/zircon/system/core/devmgr/devhost/proxy-iostate.cpp b/zircon/system/core/devmgr/devhost/proxy-iostate.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..689c4b00ab2bb6c1c93009694aecc981d70da659
--- /dev/null
+++ b/zircon/system/core/devmgr/devhost/proxy-iostate.cpp
@@ -0,0 +1,95 @@
+// 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 "proxy-iostate.h"
+
+#include <fbl/auto_lock.h>
+#include "../shared/log.h"
+#include "connection-destroyer.h"
+#include "zx-device.h"
+
+namespace devmgr {
+
+ProxyIostate::~ProxyIostate() {
+    fbl::AutoLock guard(&dev->proxy_ios_lock);
+    if (dev->proxy_ios == this) {
+        dev->proxy_ios = nullptr;
+    }
+}
+
+// Handling RPC From Proxy Devices to BusDevs
+void ProxyIostate::HandleRpc(fbl::unique_ptr<ProxyIostate> conn, async_dispatcher_t* dispatcher,
+                             async::WaitBase* wait, zx_status_t status,
+                             const zx_packet_signal_t* signal) {
+    if (status != ZX_OK) {
+        return;
+    }
+
+    if (conn->dev == nullptr) {
+        log(RPC_SDW, "proxy-rpc: stale rpc? (ios=%p)\n", conn.get());
+        // Do not re-issue the wait here
+        return;
+    }
+    if (signal->observed & ZX_CHANNEL_READABLE) {
+        log(RPC_SDW, "proxy-rpc: rpc readable (ios=%p,dev=%p)\n", conn.get(), conn->dev.get());
+        zx_status_t r = conn->dev->ops->rxrpc(conn->dev->ctx, wait->object());
+        if (r != ZX_OK) {
+            log(RPC_SDW, "proxy-rpc: rpc cb error %d (ios=%p,dev=%p)\n", r, conn.get(),
+                conn->dev.get());
+            // Let |conn| be destroyed
+            return;
+        }
+        BeginWait(std::move(conn), dispatcher);
+        return;
+    }
+    if (signal->observed & ZX_CHANNEL_PEER_CLOSED) {
+        log(RPC_SDW, "proxy-rpc: peer closed (ios=%p,dev=%p)\n", conn.get(), conn->dev.get());
+        // Let |conn| be destroyed
+        return;
+    }
+    log(ERROR, "devhost: no work? %08x\n", signal->observed);
+    BeginWait(std::move(conn), dispatcher);
+}
+
+zx_status_t ProxyIostate::Create(const fbl::RefPtr<zx_device_t>& dev, zx::channel rpc,
+                                 async_dispatcher_t* dispatcher) {
+    // This must be held for the adding of the channel to the port, since the
+    // async loop may run immediately after that point.
+    fbl::AutoLock guard(&dev->proxy_ios_lock);
+
+    if (dev->proxy_ios) {
+        dev->proxy_ios->Cancel(dispatcher);
+        dev->proxy_ios = nullptr;
+    }
+
+    auto ios = std::make_unique<ProxyIostate>();
+    if (ios == nullptr) {
+        return ZX_ERR_NO_MEMORY;
+    }
+
+    ios->dev = dev;
+    ios->set_channel(std::move(rpc));
+
+    // |ios| is will be owned by the async loop.  |dev| holds a reference that will be
+    // cleared prior to destruction.
+    dev->proxy_ios = ios.get();
+
+    zx_status_t status = BeginWait(std::move(ios), dispatcher);
+    if (status != ZX_OK) {
+        dev->proxy_ios = nullptr;
+        return status;
+    }
+
+    return ZX_OK;
+}
+
+// The device for which ProxyIostate is currently attached to should have
+// its proxy_ios_lock held across Cancel().
+void ProxyIostate::Cancel(async_dispatcher_t* dispatcher) {
+    // TODO(teisenbe): We should probably check the return code in case the
+    // queue was full
+    ConnectionDestroyer::Get()->QueueProxyConnection(dispatcher, this);
+}
+
+} // namespace devmgr
diff --git a/zircon/system/core/devmgr/devhost/proxy-iostate.h b/zircon/system/core/devmgr/devhost/proxy-iostate.h
new file mode 100644
index 0000000000000000000000000000000000000000..83615b576c36ddbbc72fd52b666ab82aaadaae71
--- /dev/null
+++ b/zircon/system/core/devmgr/devhost/proxy-iostate.h
@@ -0,0 +1,38 @@
+// 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.
+
+#pragma once
+
+#include <fbl/ref_ptr.h>
+#include <fbl/unique_ptr.h>
+#include <lib/async/cpp/wait.h>
+#include <lib/zx/channel.h>
+#include "../shared/async-loop-owned-rpc-handler.h"
+
+struct zx_device;
+
+namespace devmgr {
+
+struct ProxyIostate : AsyncLoopOwnedRpcHandler<ProxyIostate> {
+    ProxyIostate() = default;
+    ~ProxyIostate();
+
+    // Creates a ProxyIostate and points |dev| at it.  The ProxyIostate is owned
+    // by the async loop, and its destruction may be requested by calling
+    // Cancel().
+    static zx_status_t Create(const fbl::RefPtr<zx_device>& dev, zx::channel rpc,
+                              async_dispatcher_t* dispatcher);
+
+    // Request the destruction of the proxy connection
+    void Cancel(async_dispatcher_t* dispatcher);
+
+    static void HandleRpc(fbl::unique_ptr<ProxyIostate> conn, async_dispatcher_t* dispatcher,
+                          async::WaitBase* wait, zx_status_t status,
+                          const zx_packet_signal_t* signal);
+
+    fbl::RefPtr<zx_device> dev;
+};
+static void proxy_ios_destroy(const fbl::RefPtr<zx_device>& dev);
+
+} // namespace devmgr
diff --git a/zircon/system/utest/BUILD.gn b/zircon/system/utest/BUILD.gn
index 8a8ede7aa314c200fd530b17e0f77adab1a66bdc..1eb2a0824e88ff81c700bc3611e319887ccf39cc 100644
--- a/zircon/system/utest/BUILD.gn
+++ b/zircon/system/utest/BUILD.gn
@@ -20,6 +20,7 @@ if (current_cpu != "") {
       ":host",  # TODO(mcgrathr): reach this differently?
       "$zx/system/core/bootsvc:tests",
       "$zx/system/core/devmgr/devcoordinator:devcoordinator-test",
+      "$zx/system/core/devmgr/devhost:devhost-test",
       "$zx/system/core/devmgr/fshost:block-watcher-test",
       "$zx/system/core/devmgr/fshost:fshost-test",
       "$zx/system/core/virtcon:virtual-console-test",