[virtio] virtio-scsi driver

virtio-scsi is a storage controller that bridges a virtio bus to
a SCSI bus or busses; each bus may have zero to many storage
targets/Logical Units attached. virtio-scsi supports a rich per-target
command set, asynchronous event notification for disk
attach/detach/resize, 'task management' (cancellation/target reset), and
command queueing.

virtio-scsi is the default storage controller in GCE.

Currently this driver is just a skeleton - it probes every possible SCSI
target with a TEST UNIT READY command and allocates a scsi::Disk bridge
between the zircon block protocol and SCSI layer. Subsequent commits
will implement the translation in scsi::Disk, break up ExecuteCommandSync
into a call to queue a request with a callback and a mechanism to
wait for a command, and complete and pull the probe sequence from
scsi.cpp to scsilib.cpp.

Tested:
1) Created GCE instance w:
$ fx set x64 --args=kernel_cmdline_files=\[\"//scripts/gce/kernel-cmdline.txt\"\]
$ fx full-build
$ fx gce create-fuchsia-image && fx gce create-instance
<connected to serial port>
Saw:
[00007.306] 02580.02790> scsi-disk-1-0: added
[00007.316] 02580.02603> block: device 'scsi-disk-1-0': invalid block size: 0
[00007.316] 02580.02603> devhost[00:03.0/virtio-scsi/scsi-disk-1-0] bind driver '/boot/driver/block.so' failed: -2
[00007.318] 01102.01115> devcoord: rpc: bind-driver 'scsi-disk-1-0' status -2
(size zero is expected.)

2) Ran dm dump:
...
            [00:03.0] pid=2012 /boot/driver/bus-pci.so
               <00:03.0> pid=2921 /boot/driver/bus-pci.proxy.so
                  [virtio-scsi] pid=2921 /boot/driver/virtio.so
...

3) Attached four scsi disks, rebooted, saw:
[00007.431] 02572.02595> virtual zx_status_t virtio::ScsiDevice::Init(): entry
[00007.458] 02572.02756> scsi-disk-1-0: status=0
[00007.463] 02572.02756> scsi-disk-2-0: status=0
[00007.470] 02572.02756> scsi-disk-3-0: status=0
[00007.476] 02572.02756> scsi-disk-4-0: status=0
...
[00027.252] 01102.01115> devcoord: device 0x45b762509e00 name='00:04.0' disconnected!
[00027.261] 01102.01115> devcoord: device 0x45b762509c00 name='00:03.0' disconnected!
[00027.268] 01102.01115> devcoord: device 0x45b762509b00 name='00:01.3' disconnected!
[00027.270] 01102.01115> devcoord: device 0x45b762509a00 name='00:01.0' disconnected!
[00027.271] 01102.01115> devcoord: device 0x45b762509900 name='00:00.0' disconnected!

ZX-2314

Change-Id: Ic361da103d63bf3e0dad09d479340d6ca4d034e2
diff --git a/system/dev/bus/virtio/backends/fake.h b/system/dev/bus/virtio/backends/fake.h
index 3d0590f..5f9412c 100644
--- a/system/dev/bus/virtio/backends/fake.h
+++ b/system/dev/bus/virtio/backends/fake.h
@@ -42,28 +42,40 @@
         state_ = State::DEVICE_RESET;
     }
     void ReadDeviceConfig(uint16_t offset, uint8_t* value) override {
-        EXPECT_GT(registers8_.count(offset), 0);
-        *value = registers8_[offset];
+        char message[80] = {};
+        snprintf(message, sizeof(message), "offset-%xh/8", offset);
+        auto shifted_offset = static_cast<uint16_t>(offset + kISRStatus + 1);
+        EXPECT_GT(registers8_.count(shifted_offset), 0, message);
+        *value = registers8_[shifted_offset];
     }
     void ReadDeviceConfig(uint16_t offset, uint16_t* value) override {
-        EXPECT_GT(registers16_.count(offset), 0);
-        *value = registers16_[offset];
+        char message[80] = {};
+        snprintf(message, sizeof(message), "offset-%xh/16", offset);
+        auto shifted_offset = static_cast<uint16_t>(offset + kISRStatus + 1);
+        EXPECT_GT(registers16_.count(shifted_offset), 0, message);
+        *value = registers16_[shifted_offset];
     }
     void ReadDeviceConfig(uint16_t offset, uint32_t* value) override {
-        EXPECT_GT(registers32_.count(offset), 0);
-        *value = registers32_[offset];
+        char message[80] = {};
+        snprintf(message, sizeof(message), "offset-%xh/32", offset);
+        auto shifted_offset = static_cast<uint16_t>(offset + kISRStatus + 1);
+        EXPECT_GT(registers32_.count(shifted_offset), 0, message);
+        *value = registers32_[shifted_offset];
     }
     void ReadDeviceConfig(uint16_t offset, uint64_t* value) override {
         EXPECT_TRUE(0);  // Not Implemented.
     }
     void WriteDeviceConfig(uint16_t offset, uint8_t value) override {
-        registers8_[offset] = value;
+        auto shifted_offset = static_cast<uint16_t>(offset + kISRStatus + 1);
+        registers8_[shifted_offset] = value;
     }
     void WriteDeviceConfig(uint16_t offset, uint16_t value) override {
-        registers16_[offset] = value;
+        auto shifted_offset = static_cast<uint16_t>(offset + kISRStatus + 1);
+        registers16_[shifted_offset] = value;
     }
     void WriteDeviceConfig(uint16_t offset, uint32_t value) override {
-        registers32_[offset] = value;
+        auto shifted_offset = static_cast<uint16_t>(offset + kISRStatus + 1);
+        registers32_[shifted_offset] = value;
     }
     void WriteDeviceConfig(uint16_t offset, uint64_t value) override {
         EXPECT_TRUE(0);  // Not Implemented.
@@ -79,26 +91,72 @@
         EXPECT_GT(queue_sizes_.count(ring_index), 0);
         kicked_queues_.insert(ring_index);
     }
-    uint32_t IsrStatus() override { return 0; }
+    uint32_t IsrStatus() override { return registers8_.find(kISRStatus)->second; }
     zx_status_t InterruptValid() override { return ZX_OK; }
     zx_status_t WaitForInterrupt() override { return ZX_OK; }
 
   protected:
-    FakeBackend(std::initializer_list<std::pair<const uint16_t, uint8_t>> registers8,
-                std::initializer_list<std::pair<const uint16_t, uint16_t>> registers16,
-                std::initializer_list<std::pair<const uint16_t, uint32_t>> registers32,
-                std::initializer_list<std::pair<const uint16_t, uint16_t>> queue_sizes):
-        registers8_(registers8), registers16_(registers16), registers32_(registers32),
-        queue_sizes_(queue_sizes) {}
+    // virtio header register offsets.
+    static constexpr uint16_t kDeviceFeatures = 0;
+    static constexpr uint16_t kGuestFeatures = 4;
+    static constexpr uint16_t kQueueAddress = 8;
+    static constexpr uint16_t kQueueSize = 12;
+    static constexpr uint16_t kQueueSelect = 14;
+    static constexpr uint16_t kQueueNotify = 16;
+    static constexpr uint16_t kDeviceStatus = 18;
+    static constexpr uint16_t kISRStatus = 19;
 
-   // Returns true if a queue has been kicked (notified) and clears the notified bit.
-   bool QueueKicked(uint16_t queue_index) {
-     bool is_queue_kicked = (kicked_queues_.count(queue_index));
-     if (is_queue_kicked) {
-       kicked_queues_.erase(queue_index);
-     }
-     return is_queue_kicked;
-   }
+    explicit FakeBackend(std::initializer_list<std::pair<const uint16_t, uint16_t>> queue_sizes):
+        queue_sizes_(queue_sizes) {
+        // Bind standard virtio header registers into register maps.
+        registers32_.insert({kDeviceFeatures, 0});
+        registers32_.insert({kGuestFeatures, 0});
+        registers32_.insert({kQueueAddress, 0});
+        registers16_.insert({kQueueSize, 0});
+        registers16_.insert({kQueueSelect, 0});
+        registers16_.insert({kQueueNotify, 0});
+        registers8_.insert({kDeviceStatus, 0});
+        registers8_.insert({kISRStatus, 0});
+    }
+
+    // Returns true if a queue has been kicked (notified) and clears the notified bit.
+    bool QueueKicked(uint16_t queue_index) {
+        bool is_queue_kicked = (kicked_queues_.count(queue_index));
+        if (is_queue_kicked) {
+            kicked_queues_.erase(queue_index);
+        }
+        return is_queue_kicked;
+    }
+
+    template<typename T> void AddClassRegister(uint16_t offset, T value) {
+        if constexpr(sizeof(T) == 1) {
+            registers8_.insert({kISRStatus + 1 + offset, value});
+        } else if constexpr(sizeof(T) == 2) {
+            registers16_.insert({kISRStatus + 1 + offset, value});
+        } else if constexpr(sizeof(T) == 4) {
+            registers32_.insert({kISRStatus + 1 + offset, value});
+        }
+    }
+
+    template<typename T> void SetRegister(uint16_t offset, T value) {
+        if constexpr(sizeof(T) == 1) {
+            registers8_[offset] = value;
+        } else if constexpr(sizeof(T) == 2) {
+            registers16_[offset] = value;
+        } else if constexpr(sizeof(T) == 4) {
+            registers32_[offset] = value;
+        }
+    }
+
+    template<typename T> void ReadRegister(uint16_t offset, T* output) {
+        if constexpr(sizeof(T) == 1) {
+            *output = registers8_.find(offset)->second;
+        } else if constexpr(sizeof(T) == 2) {
+            *output = registers16_.find(offset)->second;
+        } else if constexpr(sizeof(T) == 4) {
+            *output = registers32_.find(offset)->second;
+        }
+    }
 
   private:
     enum class State {
diff --git a/system/dev/bus/virtio/rules.mk b/system/dev/bus/virtio/rules.mk
index c029cc2..8cee6c6 100644
--- a/system/dev/bus/virtio/rules.mk
+++ b/system/dev/bus/virtio/rules.mk
@@ -19,6 +19,8 @@
     $(LOCAL_DIR)/ring.cpp \
     $(LOCAL_DIR)/rng.cpp \
     $(LOCAL_DIR)/socket.cpp \
+    $(LOCAL_DIR)/scsi.cpp \
+    $(LOCAL_DIR)/scsilib.cpp \
     $(LOCAL_DIR)/virtio_driver.cpp \
 	$(LOCAL_DIR)/backends/pci.cpp \
 	$(LOCAL_DIR)/backends/pci_legacy.cpp \
@@ -53,3 +55,47 @@
 MODULE_LIBS := system/ulib/driver system/ulib/zircon system/ulib/c
 
 include make/module.mk
+
+# Unit tests
+MODULE := $(LOCAL_DIR).test
+MODULE_TYPE := usertest
+MODULE_SRCS := \
+    $(LOCAL_DIR)/device.cpp \
+    $(LOCAL_DIR)/scsi.cpp \
+    $(LOCAL_DIR)/scsilib.cpp \
+    $(LOCAL_DIR)/ring.cpp \
+    $(LOCAL_DIR)/scsi_test.cpp \
+    $(LOCAL_DIR)/test_main.cpp \
+
+MODULE_STATIC_LIBS := \
+    system/dev/lib/fake_ddk \
+    system/ulib/async \
+    system/ulib/async.cpp \
+    system/ulib/async-loop \
+    system/ulib/async-loop.cpp \
+    system/ulib/ddk \
+    system/ulib/ddktl \
+    system/ulib/fbl \
+    system/ulib/fidl \
+    system/ulib/hid \
+    system/ulib/hwreg \
+    system/ulib/pretty \
+    system/ulib/sync \
+    system/ulib/unittest \
+    system/ulib/virtio \
+    system/ulib/zx \
+    system/ulib/zxcpp \
+
+MODULE_LIBS := \
+    system/ulib/driver \
+    system/ulib/zircon \
+    system/ulib/c
+
+MODULE_BANJO_LIBS := \
+    system/banjo/ddk-protocol-block \
+    system/banjo/ddk-protocol-pci \
+
+MODULE_COMPILEFLAGS := \
+    -I$(LOCAL_DIR)\
+
+include make/module.mk
diff --git a/system/dev/bus/virtio/scsi.cpp b/system/dev/bus/virtio/scsi.cpp
new file mode 100644
index 0000000..afe1b4c
--- /dev/null
+++ b/system/dev/bus/virtio/scsi.cpp
@@ -0,0 +1,220 @@
+// 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 "scsi.h"
+
+#include <ddk/debug.h>
+#include <fbl/algorithm.h>
+#include <fbl/auto_call.h>
+#include <fbl/auto_lock.h>
+#include <inttypes.h>
+#include <pretty/hexdump.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+#include <virtio/scsi.h>
+#include <zircon/compiler.h>
+
+#include <utility>
+
+#include "scsilib.h"
+#include "trace.h"
+
+#define LOCAL_TRACE 0
+
+namespace virtio {
+
+// Fill in req->lun with a single-level LUN structure representing target:lun.
+static void FillLUNStructure(struct virtio_scsi_req_cmd* req, uint8_t target, uint16_t lun) {
+    req->lun[0] = 1;
+    req->lun[1] = target;
+    memcpy(&req->lun[2], &lun, sizeof(lun));
+}
+
+zx_status_t ScsiDevice::ExecuteCommandSync(uint8_t target, uint16_t lun, uint8_t* cdb,
+                                           size_t cdb_length) {
+    uint8_t* const request_buffers_addr =
+        reinterpret_cast<uint8_t*>(io_buffer_virt(&request_buffers_));
+    auto* const req = reinterpret_cast<struct virtio_scsi_req_cmd*>(request_buffers_addr);
+    auto* const resp = reinterpret_cast<struct virtio_scsi_resp_cmd*>(
+        request_buffers_addr + sizeof(struct virtio_scsi_req_cmd));
+
+    memset(req, 0, sizeof(*req));
+    memcpy(&req->cdb, cdb, cdb_length);
+    FillLUNStructure(req, /*target=*/target, /*lun=*/lun);
+
+    // virtio-scsi requests have a 'request' region, a data-out region, a
+    // 'response' region, and a data-in region. Allocate and fill them
+    // and then execute the request.
+    //
+    // TODO: Currently only allocates two regions, request/response.
+    // Add more so we can support most SCSI commands.
+    uint16_t id = 0;
+    auto request_desc = request_queue_.AllocDescChain(/*count=*/2, &id);
+    request_desc->addr = io_buffer_phys(&request_buffers_);
+    request_desc->len = sizeof(*req);
+    request_desc->flags = VRING_DESC_F_NEXT;
+
+    auto response_desc = request_queue_.DescFromIndex(request_desc->next);
+    response_desc->addr = io_buffer_phys(&request_buffers_) + sizeof(*req);
+    response_desc->len = sizeof(*resp);
+    response_desc->flags = VRING_DESC_F_WRITE;
+
+    request_queue_.SubmitChain(id);
+    request_queue_.Kick();
+
+    // Wait for request to complete.
+    sync_completion_t sync;
+    // annotalysis is unable to determine that ScsiDevice::lock_ is held when the IrqRingUpdate
+    // lambda is invoked.
+    request_queue_.IrqRingUpdate([this, &sync](vring_used_elem* elem) TA_NO_THREAD_SAFETY_ANALYSIS {
+        auto index = static_cast<uint16_t>(elem->id);
+
+        // Synchronously reclaim the entire descriptor chain.
+        for (;;) {
+            vring_desc const* desc = request_queue_.DescFromIndex(index);
+            const bool has_next = desc->flags & VRING_DESC_F_NEXT;
+            const uint16_t next = desc->next;
+
+            this->request_queue_.FreeDesc(index);
+            if (!has_next) {
+                break;
+            }
+            index = next;
+        }
+        sync_completion_signal(&sync);
+    });
+    sync_completion_wait(&sync, ZX_TIME_INFINITE);
+
+    // If there was either a transport or SCSI level error, return a failure.
+    if (resp->response || resp->status) {
+        return ZX_ERR_INTERNAL;
+    }
+
+    return ZX_OK;
+}
+
+zx_status_t ScsiDevice::WorkerThread() {
+    fbl::AutoLock lock(&lock_);
+
+    // Execute TEST UNIT READY on every possible target to find potential disks.
+    // TODO(ZX-2314): Move probe sequence to ScsiLib -- have it call down into LLDs to execute
+    // commands.
+    for (auto channel = 0u; channel < config_.max_channel; channel++) {
+        for (uint8_t target = 0u; target < config_.max_target; target++) {
+            for (uint16_t lun = 0u; lun < config_.max_lun; lun++) {
+                scsi::TestUnitReadyCDB cdb = {};
+                cdb.opcode = scsi::Opcode::TEST_UNIT_READY;
+
+                auto status = ExecuteCommandSync(
+                    /*target=*/target,
+                    /*lun=*/lun, reinterpret_cast<uint8_t*>(&cdb), sizeof(cdb));
+                if (status == ZX_OK) {
+                    scsi::Disk::Create(device_, /*target=*/target, /*lun=*/lun);
+                }
+            }
+        }
+    }
+
+    return ZX_OK;
+}
+
+zx_status_t ScsiDevice::Init() {
+    LTRACE_ENTRY;
+
+    Device::DeviceReset();
+    Device::ReadDeviceConfig<uint32_t>(offsetof(virtio_scsi_config, num_queues),
+                                       &config_.num_queues);
+    Device::ReadDeviceConfig<uint32_t>(offsetof(virtio_scsi_config, seg_max),
+                                       &config_.seg_max);
+    Device::ReadDeviceConfig<uint32_t>(offsetof(virtio_scsi_config, max_sectors),
+                                       &config_.max_sectors);
+    Device::ReadDeviceConfig<uint32_t>(offsetof(virtio_scsi_config, cmd_per_lun),
+                                       &config_.cmd_per_lun);
+    Device::ReadDeviceConfig<uint32_t>(offsetof(virtio_scsi_config, event_info_size),
+                                       &config_.event_info_size);
+    Device::ReadDeviceConfig<uint32_t>(offsetof(virtio_scsi_config, sense_size),
+                                       &config_.sense_size);
+    Device::ReadDeviceConfig<uint32_t>(offsetof(virtio_scsi_config, cdb_size),
+                                       &config_.cdb_size);
+    Device::ReadDeviceConfig<uint16_t>(offsetof(virtio_scsi_config, max_channel),
+                                       &config_.max_channel);
+    Device::ReadDeviceConfig<uint16_t>(offsetof(virtio_scsi_config, max_target),
+                                       &config_.max_target);
+    Device::ReadDeviceConfig<uint32_t>(offsetof(virtio_scsi_config, max_lun),
+                                       &config_.max_lun);
+
+    Device::DriverStatusAck();
+
+    {
+        fbl::AutoLock lock(&lock_);
+        auto err = control_ring_.Init(/*index=*/Queue::CONTROL);
+        if (err) {
+            zxlogf(ERROR, "failed to allocate control queue\n");
+            return err;
+        }
+
+        err = request_queue_.Init(/*index=*/Queue::REQUEST);
+        if (err) {
+            zxlogf(ERROR, "failed to allocate request queue\n");
+            return err;
+        }
+
+        // Allocate one virtio_scsi_req_cmd / virtio_scsi_resp_cmd per request
+        // queue entry.
+        const size_t request_buffers_size =
+            Device::GetRingSize(Queue::REQUEST) *
+            (sizeof(struct virtio_scsi_req_cmd) + sizeof(struct virtio_scsi_resp_cmd));
+        auto status =
+            io_buffer_init(&request_buffers_, bti().get(),
+                           /*size=*/request_buffers_size, IO_BUFFER_RW | IO_BUFFER_CONTIG);
+        if (status) {
+            zxlogf(ERROR, "failed to allocate queue working memory\n");
+            return status;
+        }
+    }
+
+    Device::StartIrqThread();
+    Device::DriverStatusOk();
+
+    device_add_args_t args{};
+    args.version = DEVICE_ADD_ARGS_VERSION;
+    args.name = "virtio-scsi";
+    args.ops = &device_ops_;
+    args.ctx = this;
+
+    // Synchronize against Unbind()/Release() before the worker thread is running.
+    fbl::AutoLock lock(&lock_);
+    auto status = device_add(Device::bus_device_, &args, &device_);
+    if (status != ZX_OK) {
+        return status;
+    }
+
+    auto td = [](void* ctx) {
+        ScsiDevice* const device = static_cast<ScsiDevice*>(ctx);
+        return device->WorkerThread();
+    };
+    int ret = thrd_create_with_name(&worker_thread_, td, this, "virtio-scsi-worker");
+    if (ret != thrd_success) {
+        return ZX_ERR_INTERNAL;
+    }
+
+    return status;
+}
+
+void ScsiDevice::Unbind() {
+    Device::Unbind();
+}
+
+void ScsiDevice::Release() {
+    {
+        fbl::AutoLock lock(&lock_);
+        worker_thread_should_exit_ = true;
+    }
+    thrd_join(worker_thread_, nullptr);
+    Device::Release();
+}
+
+} // namespace virtio
diff --git a/system/dev/bus/virtio/scsi.h b/system/dev/bus/virtio/scsi.h
new file mode 100644
index 0000000..414abd0
--- /dev/null
+++ b/system/dev/bus/virtio/scsi.h
@@ -0,0 +1,65 @@
+// 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 "device.h"
+#include "ring.h"
+
+#include <atomic>
+#include <stdlib.h>
+
+#include "backends/backend.h"
+#include <lib/sync/completion.h>
+#include <virtio/scsi.h>
+#include <zircon/compiler.h>
+#include <zircon/thread_annotations.h>
+
+namespace virtio {
+
+class ScsiDevice : public Device {
+public:
+    enum Queue {
+        CONTROL = 0,
+        EVENT = 1,
+        REQUEST = 2,
+    };
+
+    ScsiDevice(zx_device_t* device, zx::bti bti, fbl::unique_ptr<Backend> backend)
+        : Device(device, std::move(bti), std::move(backend)) {}
+
+    // virtio::Device overrides
+    zx_status_t Init() override;
+    void Unbind() override;
+    void Release() override;
+    // Invoked for most device interrupts.
+    void IrqRingUpdate() override {}
+    // Invoked on config change interrupts.
+    void IrqConfigChange() override {}
+
+    const char* tag() const override { return "virtio-scsi"; }
+
+private:
+    zx_status_t ExecuteCommandSync(uint8_t target, uint16_t lun, uint8_t* cdb, size_t cdb_length)
+        TA_REQ(lock_);
+
+    zx_status_t WorkerThread();
+
+    // Latched copy of virtio-scsi device configuration.
+    struct virtio_scsi_config config_ TA_GUARDED(lock_) = {};
+
+    // DMA Memory for virtio-scsi requests/responses, events, task management functions.
+    io_buffer_t request_buffers_ TA_GUARDED(lock_) = {};
+
+    Ring control_ring_ TA_GUARDED(lock_) = {this};
+    Ring request_queue_ TA_GUARDED(lock_) = {this};
+
+    thrd_t worker_thread_;
+    bool worker_thread_should_exit_ TA_GUARDED(lock_) = {};
+
+    // Synchronizes virtio rings and worker thread control.
+    fbl::Mutex lock_;
+};
+
+} // namespace virtio
diff --git a/system/dev/bus/virtio/scsi_test.cpp b/system/dev/bus/virtio/scsi_test.cpp
new file mode 100644
index 0000000..9709d05
--- /dev/null
+++ b/system/dev/bus/virtio/scsi_test.cpp
@@ -0,0 +1,49 @@
+// 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/fake_ddk/fake_ddk.h>
+#include <unittest/unittest.h>
+
+#include "backends/fake.h"
+#include "scsi.h"
+
+using Queue = virtio::ScsiDevice::Queue;
+
+namespace {
+
+// Fake virtio 'backend' for a virtio-scsi device.
+class FakeBackendForScsi : public virtio::FakeBackend {
+  public:
+    FakeBackendForScsi() : virtio::FakeBackend(
+        /*queue_sizes=*/{{Queue::CONTROL, 128}, {Queue::REQUEST, 128}, {Queue::EVENT, 128}}) {
+        // TODO(venkateshs): Sane defaults for these registers.
+        AddClassRegister(offsetof(virtio_scsi_config, num_queues), 1);
+        AddClassRegister(offsetof(virtio_scsi_config, seg_max), 1);
+        AddClassRegister(offsetof(virtio_scsi_config, max_sectors), 1);
+        AddClassRegister(offsetof(virtio_scsi_config, cmd_per_lun), 1);
+        AddClassRegister(offsetof(virtio_scsi_config, event_info_size), 1);
+        AddClassRegister(offsetof(virtio_scsi_config, sense_size), 1);
+        AddClassRegister(offsetof(virtio_scsi_config, cdb_size), 1);
+        AddClassRegister(offsetof(virtio_scsi_config, max_channel), static_cast<uint16_t>(1));
+        AddClassRegister(offsetof(virtio_scsi_config, max_target), static_cast<uint16_t>(1));
+        AddClassRegister(offsetof(virtio_scsi_config, max_lun), 1);
+    }
+};
+
+bool InitTest() {
+    BEGIN_TEST;
+    fbl::unique_ptr<virtio::Backend> backend = fbl::make_unique<FakeBackendForScsi>();
+    zx::bti bti(ZX_HANDLE_INVALID);
+
+    virtio::ScsiDevice scsi(/*parent=*/nullptr, std::move(bti), std::move(backend));
+    auto status = scsi.Init();
+    EXPECT_NE(status, ZX_OK);
+    END_TEST;
+}
+
+}  // anonymous namespace
+
+BEGIN_TEST_CASE(ScsiDriverTests)
+RUN_TEST_SMALL(InitTest)
+END_TEST_CASE(ScsiDriverTests)
diff --git a/system/dev/bus/virtio/scsilib.cpp b/system/dev/bus/virtio/scsilib.cpp
new file mode 100644
index 0000000..033e379
--- /dev/null
+++ b/system/dev/bus/virtio/scsilib.cpp
@@ -0,0 +1,34 @@
+// Copyriht 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 "scsilib.h"
+
+#include <ddk/protocol/block.h>
+#include <fbl/alloc_checker.h>
+
+namespace scsi {
+
+zx_status_t Disk::Create(zx_device_t* parent, uint8_t target, uint16_t lun) {
+    fbl::AllocChecker ac;
+    auto* const disk = new (&ac) scsi::Disk(parent, /*target=*/target, /*lun=*/lun);
+    if (!ac.check()) {
+        return ZX_ERR_NO_MEMORY;
+    }
+    auto status = disk->Bind();
+    if (status != ZX_OK) {
+        delete disk;
+    }
+    return status;
+}
+
+zx_status_t Disk::Bind() {
+    return DdkAdd(tag_);
+}
+
+Disk::Disk(zx_device_t* parent, uint8_t target, uint16_t lun)
+    : DeviceType(parent), target_(target), lun_(lun) {
+    snprintf(tag_, sizeof(tag_), "scsi-disk-%d-%d", target_, lun_);
+}
+
+} // namespace scsi
diff --git a/system/dev/bus/virtio/scsilib.h b/system/dev/bus/virtio/scsilib.h
new file mode 100644
index 0000000..ea308d0
--- /dev/null
+++ b/system/dev/bus/virtio/scsilib.h
@@ -0,0 +1,76 @@
+// 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 <ddk/device.h>
+#include <ddk/driver.h>
+#include <ddk/protocol/block.h>
+#include <ddktl/device.h>
+#include <ddktl/protocol/block.h>
+#include <stdint.h>
+
+namespace scsi {
+
+enum class Opcode : uint8_t {
+    TEST_UNIT_READY = 0x00,
+    INQUIRY = 0x12,
+    MODE_SENSE_6 = 0x1A,
+    READ_16 = 0x88,
+    WRITE_16 = 0x8A,
+};
+
+// SCSI command structures (CDBs)
+
+struct TestUnitReadyCDB {
+    Opcode opcode;
+    uint8_t reserved[4];
+    uint8_t control;
+} __PACKED;
+
+static_assert(sizeof(TestUnitReadyCDB) == 6, "TestUnitReady CDB must be 6 bytes");
+
+class Disk;
+using DeviceType = ddk::Device<Disk, ddk::GetSizable, ddk::Unbindable>;
+
+// |Disk| represents a single SCSI direct access block device.
+// |Disk| bridges between the Zircon block protocol and SCSI commands/responses.
+class Disk : public DeviceType, public ddk::BlockImplProtocol<Disk, ddk::base_protocol> {
+  public:
+    // Public so that we can use make_unique.
+    // Clients should use Disk::Create().
+    Disk(zx_device_t* parent, uint8_t target, uint16_t lun);
+
+    // Create a Disk at a specific target/lun.
+    static zx_status_t Create(zx_device_t* parent, uint8_t target, uint16_t lun);
+
+    const char* tag() const { return tag_; }
+
+    // DeviceType functions.
+    void DdkUnbind() { DdkRemove(); }
+    void DdkRelease() { delete this; }
+
+    // ddk::GetSizable functions.
+    zx_off_t DdkGetSize() { return 0; }
+
+    // ddk::BlockImplProtocol functions.
+    // TODO(ZX-2314): Implement these two functions.
+    void BlockImplQuery(block_info_t* info_out, size_t* block_op_size_out) {}
+    void BlockImplQueue(block_op_t* operation, block_impl_queue_callback completion_cb,
+                        void* cookie) {
+        completion_cb(cookie, ZX_ERR_NOT_SUPPORTED, operation);
+    }
+
+    Disk(const Disk&) = delete;
+    Disk& operator=(const Disk&) = delete;
+
+  private:
+    zx_status_t Bind();
+
+    char tag_[24];
+    const uint8_t target_;
+    const uint16_t lun_;
+};
+
+} // namespace scsi
diff --git a/system/dev/bus/virtio/test_main.cpp b/system/dev/bus/virtio/test_main.cpp
new file mode 100644
index 0000000..faff464
--- /dev/null
+++ b/system/dev/bus/virtio/test_main.cpp
@@ -0,0 +1,9 @@
+// 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 <unittest/unittest.h>
+
+int main(int argc, char** argv) {
+    return unittest_run_all_tests(argc, argv) ? 0 : -1;
+}
diff --git a/system/dev/bus/virtio/virtio_driver.cpp b/system/dev/bus/virtio/virtio_driver.cpp
index f4b6bfd..f9da9a8 100644
--- a/system/dev/bus/virtio/virtio_driver.cpp
+++ b/system/dev/bus/virtio/virtio_driver.cpp
@@ -29,6 +29,7 @@
 #include "gpu.h"
 #include "input.h"
 #include "rng.h"
+#include "scsi.h"
 #include "socket.h"
 
 static zx_status_t virtio_pci_bind(void* ctx, zx_device_t* bus_device) {
@@ -106,6 +107,11 @@
         virtio_device.reset(new virtio::SocketDevice(bus_device, std::move(bti),
                                                      std::move(backend)));
         break;
+    case VIRTIO_DEV_TYPE_SCSI:
+    case VIRTIO_DEV_TYPE_T_SCSI_HOST:
+        virtio_device.reset(new virtio::ScsiDevice(bus_device, std::move(bti),
+                                                   std::move(backend)));
+        break;
     default:
         return ZX_ERR_NOT_SUPPORTED;
     }
@@ -128,17 +134,19 @@
     .release = nullptr,
 };
 
-ZIRCON_DRIVER_BEGIN(virtio, virtio_driver_ops, "zircon", "0.1", 14)
+ZIRCON_DRIVER_BEGIN(virtio, virtio_driver_ops, "zircon", "0.1", 16)
     BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PCI),
     BI_ABORT_IF(NE, BIND_PCI_VID, VIRTIO_PCI_VENDOR_ID),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_BLOCK),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_CONSOLE),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_ENTROPY),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_NETWORK),
+    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_SCSI),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_BLOCK),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_CONSOLE),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_ENTROPY),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_NETWORK),
+    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_SCSI_HOST),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_GPU),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_INPUT),
     BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_SOCKET),
diff --git a/system/ulib/virtio/include/virtio/scsi.h b/system/ulib/virtio/include/virtio/scsi.h
new file mode 100644
index 0000000..3b7b403
--- /dev/null
+++ b/system/ulib/virtio/include/virtio/scsi.h
@@ -0,0 +1,77 @@
+// Copyright 2018 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.
+
+// virtio-scsi device ABI
+// Reference: https://ozlabs.org/~rusty/virtio-spec/virtio-0.9.5.pdf,
+//   Appendix I
+
+#pragma once
+
+#include <assert.h>
+#include <stdint.h>
+#include <zircon/compiler.h>
+
+__BEGIN_CDECLS
+
+struct virtio_scsi_config {
+    // Number of request (SCSI Command) queues
+    uint32_t num_queues;
+    uint32_t seg_max;
+    uint32_t max_sectors;
+    uint32_t cmd_per_lun;
+    uint32_t event_info_size;
+    uint32_t sense_size;
+    uint32_t cdb_size;
+    uint16_t max_channel;
+    uint16_t max_target;
+    uint32_t max_lun;
+} __PACKED;
+
+static_assert(sizeof(struct virtio_scsi_config) == 36,
+              "virtio_scsi_config should be 36 bytes.");
+
+#define VIRTIO_SCSI_CDB_DEFAULT_SIZE   32
+#define VIRTIO_SCSI_SENSE_DEFAULT_SIZE 96
+
+// A virtio-scsi request represents a single SCSI command to a single target.
+// The command command has a 'virtio_scsi_req_cmd' from the driver to the
+// device, an optional data out region (again from the driver to the device),
+// a virtio_scsi_resp_cmd from the device to the driver with Sense information
+// (if any), and an optional data in region.
+//
+// The virtio_scsi_req_cmd and resp_cmd structures must be in a single virtio
+// element unless the F_ANY_LAYOUT feature is negotiated.
+struct virtio_scsi_req_cmd {
+    uint8_t lun[8];
+    // tag must be unique for all commands issued to a LUN
+    uint64_t id;
+    // SIMPLE, ORDERED, HEAD OF QUEUE, or ACA; virtio-scsi only supports SIMPLE
+    uint8_t task_attr;
+    uint8_t prio;
+    uint8_t crn;
+    uint8_t cdb[VIRTIO_SCSI_CDB_DEFAULT_SIZE];
+} __PACKED;
+
+static_assert(sizeof(virtio_scsi_req_cmd) == 51,
+              "virtio_scsi_req_cmd should be 51 bytes");
+
+struct virtio_scsi_resp_cmd {
+    uint32_t sense_len;
+    uint32_t residual;
+    uint16_t status_qualifier;
+    uint8_t status;
+    // Transport-level command response, not SCSI command status.
+    // See: ScsiResponse
+    uint8_t response;
+    uint8_t sense[VIRTIO_SCSI_SENSE_DEFAULT_SIZE];
+} __PACKED;
+
+static_assert(sizeof(virtio_scsi_resp_cmd) == 108,
+              "virtio_scsi_resp_cmd should be 108 bytes");
+
+enum class ScsiResponse : uint8_t {
+    VIRTIO_SCSI_S_OK = 0,
+};
+
+__END_CDECLS