The Pedigree Project 0.1
VirtioScsiController.cc
1/* Copyright (c) 2026, Pedigree Developers. SPDX-License-Identifier: ISC */
2#include "VirtioScsiController.h"
3#include "pedigree/kernel/LockGuard.h"
4#include "pedigree/kernel/Log.h"
5#include "pedigree/kernel/TargetInfo.h"
6#include "pedigree/kernel/machine/IrqManager.h"
7#include "pedigree/kernel/machine/Machine.h"
8#include "pedigree/kernel/panic.h"
9#include "pedigree/kernel/process/TerminationDeferral.h"
10#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
11#include "pedigree/kernel/processor/VirtualAddressSpace.h"
12#include "pedigree/kernel/time/Time.h"
13#include "pedigree/kernel/utilities/utility.h"
14
15#include <stddef.h>
16
17#include "modules/drivers/common/InterruptProbe.h"
18
19namespace {
20constexpr size_t MaxCdbBytes = 32;
21constexpr size_t MaxSenseBytes = 96;
22constexpr size_t ResponseOffset = 128;
23
24struct VirtioScsiRequest {
25 uint8_t lun[8];
26 uint64_t id;
27 uint8_t taskAttr;
28 uint8_t priority;
29 uint8_t crn;
30 uint8_t cdb[MaxCdbBytes];
31} PACKED;
32static_assert(offsetof(VirtioScsiRequest, cdb) == 19);
33
34struct VirtioScsiResponse {
35 uint32_t senseLength;
36 uint32_t residual;
37 uint16_t statusQualifier;
38 uint8_t status;
39 uint8_t response;
40 uint8_t sense[MaxSenseBytes];
41} PACKED;
42static_assert(offsetof(VirtioScsiResponse, sense) == 12);
43static_assert(ResponseOffset >= sizeof(VirtioScsiRequest));
44
45constexpr size_t ResponseHeaderBytes = offsetof(VirtioScsiResponse, sense);
46} // namespace
47
48VirtioScsiController::VirtioScsiController(Device* pci)
49 : m_Pci(pci),
50 m_Transport(pci),
51 m_Control("Virtio SCSI request"),
52 m_Data("Virtio SCSI data"),
53 m_Completion(0, false),
54 m_Irq(0),
55 m_Targets{},
56 m_UnitCount(0),
57 m_CdbBytes(0),
58 m_SenseBytes(0),
59 m_CommandUsed(0),
60 m_InterruptCompletions(0),
61 m_TransportInitialised(false),
62 m_Ready(false),
63 m_CommandSeen(false),
64 m_CommandValid(false),
65 m_Stopping(false),
66 m_Failed(false),
67 m_Shutdown(false) {
68 setSpecificType(String("virtio-scsi-controller"));
69}
70
71VirtioScsiController::~VirtioScsiController() {
72 shutdown();
73}
74
75bool VirtioScsiController::initialiseController() {
76 if (!m_Pci || m_Pci->getPciVendorId() != 0x1af4 ||
77 (m_Pci->getPciDeviceId() != 0x1048 && m_Pci->getPciDeviceId() != 0x1004) ||
78 !m_Transport.initialise()) {
79 return false;
80 }
81 m_TransportInitialised = true;
82 if (!m_Transport.negotiate(0) || !m_Transport.setupQueue(2, m_RequestQueue) ||
83 m_RequestQueue.depth() < 3) {
84 return false;
85 }
86
87 uint32_t cdbBytes = 0, senseBytes = 0;
88 uint16_t maxTarget = 0;
89 if (!m_Transport.readDeviceConfig32(24, cdbBytes) ||
90 !m_Transport.readDeviceConfig32(20, senseBytes) ||
91 !m_Transport.readDeviceConfig16(30, maxTarget) || cdbBytes < 16 || cdbBytes > MaxCdbBytes ||
92 !senseBytes || senseBytes > MaxSenseBytes) {
93 ERROR("Virtio SCSI: unsupported command or sense size");
94 return false;
95 }
96 m_CdbBytes = cdbBytes;
97 m_SenseBytes = senseBytes;
98
99 auto& memory = PhysicalMemoryManager::instance();
101 if (!memory.allocateRegion(m_Control, 1, PhysicalMemoryManager::continuous, flags) ||
102 !memory.allocateRegion(m_Data, 1, PhysicalMemoryManager::continuous, flags)) {
103 return false;
104 }
105 ByteSet(m_Control.virtualAddress(), 0, TargetInfo::getPageSize());
106 ByteSet(m_Data.virtualAddress(), 0, TargetInfo::getPageSize());
107
108 m_Irq = m_Transport.registerInterrupt(this);
109 if (!m_Irq) {
110 ERROR("Virtio SCSI: could not register PCI interrupt");
111 return false;
112 }
113 if (!m_Transport.ready()) {
114 return false;
115 }
116 m_Ready = true;
117
118 uint8_t inquiry[36]{};
119 const uint8_t inquiryCdb[6] = {0x12, 0, 0, 0, sizeof(inquiry), 0};
120 if (!InterruptProbe::run(
121 [&] {
122 return command(0, inquiryCdb, sizeof(inquiryCdb), inquiry, sizeof(inquiry), false,
123 true) != Result::TransportError;
124 },
125 [&] {
126 LockGuard<Mutex> guard(m_IrqLock);
127 return m_InterruptCompletions;
128 })) {
129 ERROR("Virtio SCSI: interrupt delivery probe failed");
130 return false;
131 }
132
133 // A missing target returns a normal virtio completion, so probe without
134 // creating ScsiDisk instances that would log each absent target as an error.
135 const unsigned lastTarget = maxTarget > 255 ? 255 : maxTarget;
136 for (unsigned target = 0; target <= lastTarget; ++target) {
137 if (command(static_cast<uint8_t>(target), inquiryCdb, sizeof(inquiryCdb), inquiry,
138 sizeof(inquiry), false, false) == Result::Success) {
139 m_Targets[m_UnitCount++] = static_cast<uint8_t>(target);
140 }
141 if (m_Failed) {
142 return false;
143 }
144 }
145 if (!m_UnitCount) {
146 return false;
147 }
148 searchDisks();
149 NOTICE("Virtio SCSI: " << Dec << getNumChildren() << " disks, shared INTx" << Hex);
150 return getNumChildren() != 0;
151}
152
153void VirtioScsiController::drainCompletions(bool fromInterrupt) {
154 LockGuard<Mutex> guard(m_IrqLock);
155 if (m_Stopping) {
156 return;
157 }
158 Virtio::Completion completion{};
159 while (m_RequestQueue.pop(completion)) {
160 m_CommandValid = completion.cookie == this && completion.length >= ResponseHeaderBytes;
161 m_CommandUsed = completion.length;
162 m_CommandSeen = true;
163 if (fromInterrupt) {
164 ++m_InterruptCompletions;
165 }
166 m_Completion.release();
167 }
168}
169
171 LockGuard<Mutex> guard(m_IrqLock);
172 if (m_Stopping) {
174 }
175 const uint8_t isr = m_Transport.readIsr();
176 if (!isr) {
177 return IrqDisposition::NotHandled;
178 }
179 if (isr & 1U) {
180 Virtio::Completion completion{};
181 while (m_RequestQueue.pop(completion)) {
182 m_CommandValid = completion.cookie == this && completion.length >= ResponseHeaderBytes;
183 m_CommandUsed = completion.length;
184 m_CommandSeen = true;
185 ++m_InterruptCompletions;
186 m_Completion.release();
187 }
188 }
189 return IrqDisposition::Handled;
190}
191
192void VirtioScsiController::failController() {
193 {
194 LockGuard<Mutex> guard(m_IrqLock);
195 if (m_Failed) {
196 return;
197 }
198 m_Stopping = true;
199 m_Failed = true;
200 }
201 if (!m_Transport.reset()) {
202 panic("Virtio SCSI: device did not stop DMA after command failure");
203 }
204 m_Ready = false;
205 m_RequestQueue.stop();
206}
207
208VirtioScsiController::Result VirtioScsiController::command(uint8_t target, const void* cdb,
209 size_t cdbBytes, void* data,
210 size_t dataBytes, bool writing,
211 bool interruptProbe) {
212 TerminationDeferral lifetime;
213 LockGuard<Mutex> serial(m_CommandLock);
214 if (!m_Ready || m_Failed || !cdb || !cdbBytes || cdbBytes > m_CdbBytes ||
215 dataBytes > TargetInfo::getPageSize() || (dataBytes && !data)) {
216 return Result::TransportError;
217 }
218
219 auto* request = static_cast<VirtioScsiRequest*>(m_Control.virtualAddress());
220 auto* response = reinterpret_cast<VirtioScsiResponse*>(
221 static_cast<uint8_t*>(m_Control.virtualAddress()) + ResponseOffset);
222 ByteSet(request, 0, sizeof(VirtioScsiRequest));
223 ByteSet(response, 0, sizeof(VirtioScsiResponse));
224 request->lun[0] = 1;
225 request->lun[1] = target;
226 request->lun[2] = 0x40;
227 request->id = HOST_TO_LITTLE64(1);
228 MemoryCopy(request->cdb, cdb, cdbBytes);
229 if (writing && dataBytes) {
230 MemoryCopy(m_Data.virtualAddress(), data, dataBytes);
231 }
232
233 const uint32_t responseBytes = ResponseHeaderBytes + m_SenseBytes;
234 Virtio::Buffer descriptors[3] = {
235 {m_Control.physicalAddress(),
236 static_cast<uint32_t>(offsetof(VirtioScsiRequest, cdb) + m_CdbBytes), false},
237 {m_Control.physicalAddress() + ResponseOffset, responseBytes, true},
238 {m_Data.physicalAddress(), static_cast<uint32_t>(dataBytes), !writing},
239 };
240 size_t descriptorCount = dataBytes ? 3 : 2;
241 if (writing && dataBytes) {
242 descriptors[1] = descriptors[2];
243 descriptors[1].deviceWrites = false;
244 descriptors[2] = {m_Control.physicalAddress() + ResponseOffset, responseBytes, true};
245 }
246
247 {
248 LockGuard<Mutex> guard(m_IrqLock);
249 m_CommandSeen = false;
250 m_CommandValid = false;
251 m_CommandUsed = 0;
252 [[maybe_unused]] const size_t stale = m_Completion.drainAvailable();
253 }
254 FENCE();
255 if (!m_RequestQueue.submit(descriptors, descriptorCount, this)) {
256 failController();
257 return Result::TransportError;
258 }
259 m_Transport.notify(2);
260
261 const auto deadline = Time::getTicks() + 30 * Time::Multiplier::Second;
262 bool first = true;
263 for (;;) {
264 const bool grace = first && interruptProbe;
265 const bool signalled = m_Completion.acquireForCompletion(1, grace ? 1 : 0, grace ? 0 : 10000);
266 first = false;
267 if (!signalled) {
268 drainCompletions(false);
269 }
270 bool seen = false, valid = false;
271 size_t used = 0;
272 {
273 LockGuard<Mutex> guard(m_IrqLock);
274 seen = m_CommandSeen;
275 valid = m_CommandValid;
276 used = m_CommandUsed;
277 }
278 if (seen) {
279 FENCE();
280 if (!valid) {
281 ERROR("Virtio SCSI: malformed completion");
282 failController();
283 return Result::TransportError;
284 }
285 if (response->response || response->status || LITTLE_TO_HOST32(response->residual) ||
286 LITTLE_TO_HOST32(response->senseLength) > m_SenseBytes) {
287 return Result::DeviceError;
288 }
289 if (!writing && dataBytes) {
290 if (used < ResponseHeaderBytes + dataBytes) {
291 ERROR("Virtio SCSI: short read completion");
292 failController();
293 return Result::TransportError;
294 }
295 MemoryCopy(data, m_Data.virtualAddress(), dataBytes);
296 }
297 return Result::Success;
298 }
299 if (Time::getTicks() >= deadline) {
300 ERROR("Virtio SCSI: command timeout");
301 failController();
302 return Result::TransportError;
303 }
304 }
305}
306
307bool VirtioScsiController::sendCommand(size_t unit, uintptr_t cdb, uint8_t cdbBytes, uintptr_t data,
308 uint16_t dataBytes, bool writing) {
309 if (unit >= m_UnitCount) {
310 return false;
311 }
312 return command(m_Targets[unit], reinterpret_cast<const void*>(cdb), cdbBytes,
313 reinterpret_cast<void*>(data), dataBytes, writing, false) == Result::Success;
314}
315
316void VirtioScsiController::shutdown() {
317 if (m_Shutdown) {
318 return;
319 }
322 {
323 LockGuard<Mutex> serial(m_CommandLock);
324 {
325 LockGuard<Mutex> guard(m_IrqLock);
326 m_Stopping = true;
327 }
328 if (m_TransportInitialised && !m_Transport.reset()) {
329 panic("Virtio SCSI: device did not stop DMA during shutdown");
330 }
331 m_Ready = false;
332 m_RequestQueue.stop();
333 }
334 if (m_Irq && !Machine::instance().getIrqManager()->unregisterHandler(m_Irq, this)) {
335 panic("Virtio SCSI: synchronous interrupt retirement failed");
336 }
337 m_Irq = 0;
338 m_Shutdown = true;
339}
uint16_t getPciDeviceId()
Definition Device.h:228
size_t getNumChildren()
Definition Device.cc:143
uint16_t getPciVendorId()
Definition Device.h:224
void * virtualAddress() const
physical_uintptr_t physicalAddress() const
static PhysicalMemoryManager & instance()
virtual void destroy()
void shutdownDiskCaches()
void release(size_t n=1)
Definition Semaphore.cc:549
MUST_USE_RESULT size_t drainAvailable()
Definition Semaphore.cc:531
MUST_USE_RESULT bool acquireForCompletion(size_t n=1, size_t timeoutSecs=0, size_t timeoutUsecs=0)
Definition Semaphore.cc:372
static constexpr size_t getPageSize() noexcept
Definition TargetInfo.h:40
IrqDisposition irq(irq_id_t number) override
void EXPORTED_PUBLIC panic(const char *msg) NORETURN
Definition panic.cc:117
@ Dec
Definition Log.h:126
@ Hex
Definition Log.h:124
IrqDisposition
Definition IrqHandler.h:31