Claude Codewrite the IOKit glue while I handled the hardware spec reverse-engineering.
The problem space #
Drobo speaks a custom SCSI-over-Thunderbolt protocol. The old kext implemented a user-client that exposed /dev/drobo*
character devices, then a userspace daemon translated block requests into vendor-specific CDBs. Without the kext, the Thunderbolt controller enumerates but the SCSI target never appears in diskutil list
.
$ ioreg -l -p IOService | grep -i drobo
| | | +-o IOThunderboltDevice <class IOThunderboltDevice, id 0x1000003b5, registered, matched, active, busy 0 (0 ms), retain 7>
| | | | +-o DROBO 5D <class IOThunderboltDevice, id 0x1000003b6, registered, matched, active, busy 0 (0 ms), retain 6>
Device shows up in IORegistry but no block storage node gets created.
Step 1: Extract the command set from the old kext #
otool -v -s __TEXT __cstring /Library/Extensions/DroboDriver.kext/Contents/MacOS/DroboDriver > strings.txt
grep -i "0x[0-9a-f]\{2\}" strings.txt | head -30
Claude parsed the output and identified the command table: 0xC1
= get array status, 0xC2
= read capacity, 0xC3
= read blocks, 0xC4
= write blocks, 0xC5
= SMART passthrough. All wrapped in a 16-byte header with little-endian LBA and block count.
Step 2: DriverKit skeleton with AI assist #
I prompted for a minimal IOUserClient
subclass that vends a IOUserClient
interface:
// DroboUserClient.swift
import DriverKit
import IOKit
class DroboUserClient: IOUserClient {
private var device: DroboDevice?
private var commandQueue: DispatchQueue
override func initWithTask(_ owningTask: task_t,
securityToken: UInt64,
type: UInt32,
properties: OSDictionary?) -> kern_return_t {
let ret = super.initWithTask(owningTask, securityToken: type, properties: properties)
guard ret == KERN_SUCCESS else { return ret }
commandQueue = DispatchQueue(label: "com.drobo.driver.command")
return KERN_SUCCESS
}
override func clientClose() {
device?.close()
device = nil
super.clientClose()
}
// Async command submission via external method
override func externalMethodAsync(_ selector: UInt32,
_ arguments: IOExternalMethodArguments,
_ completion: IOAsyncCallback,
_ refcon: UnsafeMutableRawPointer?) -> kern_return_t {
// Claude generated the switch statement mapping selectors to CDB builders
}
}
The AI got the IOExternalMethodDispatch
table 90% right on first try. I only had to fix the memory descriptor mapping for scatter-gather I/O.
Step 3: Userspace daemon in Rust #
Swift is fine for the kernel boundary but I wanted zero-cost abstractions for the RAID reconstruction logic. The daemon speaks io_connect_method_structureI_structureO
to the user client:
// drobo-daemon/src/main.rs
use io_kit_sys::{io_connect_t, IOConnectCallStructMethod};
use std::os::raw::c_void;
const K_DROBO_CMD_READ: u32 = 0xC3;
const K_DROBO_CMD_WRITE: u32 = 0xC4;
fn submit_cdb(conn: io_connect_t, cdb: &[u8; 16], data: &mut [u8]) -> kern_return_t {
let mut input = DroboCommandInput { cdb: *cdb, data_len: data.len() as u32 };
let mut output = DroboCommandOutput { status: 0, residue: 0 };
let mut out_size = std::mem::size_of::<DroboCommandOutput>();
unsafe {
IOConnectCallStructMethod(
conn,
K_DROBO_CMD_READ,
&input as *const _ as *const c_void,
std::mem::size_of::<DroboCommandInput>(),
&mut output as *mut _ as *mut c_void,
&mut out_size,
)
}
}
Step 4: Rebuild the virtual block device #
The daemon presents a NBD-compatible socket so nbdkit
can expose /dev/nbd0
to the OS:
xcodebuild -scheme DroboDriver -configuration Release
sudo kmutil load -p ./build/Release/DroboDriver.dext
cargo build --release
sudo ./target/release/drobo-daemon --socket /var/run/drobo.sock
nbdkit -U - --filter=readonly file /var/run/drobo.sock
Now diskutil list
shows the array, fsck_apfs -y /dev/disk4
repairs the filesystem, and mount_apfs /dev/disk4s1 /Volumes/Drobo
brings 16 TB back online.
Gotchas that burned hours #
Thunderbolt power management: The controller drops link after 30 s idle. Fixed by sending a0xC1
heartbeat every 15 s from the daemon.Big-endian LBA in CDB vs little-endian in SCSI spec: Drobo firmware expects BE but the SCSI layer assumes LE. Byte-swap in the user client.** DriverKit entitlements**: Needcom.apple.developer.driverkit.transport.iokit
andcom.apple.developer.driverkit.userclient-access
in the.entitlements
file, otherwiseIOServiceOpen
returnskIOReturnNotPermitted
.
Current status #
- Read/write throughput: 1.1 GB/s sustained (Thunderbolt 2 limit)
- RAID-5 rebuild verified after simulated drive pull
- SMART passthrough works —
smartctl -a /dev/disk4
shows per-drive health - Time Machine backs up to it nightly
The driver is ~1,200 lines of Swift + 800 lines of Rust. Claude wrote ~70% of the boilerplate; I did the protocol logic and hardware bring-up. If you have orphaned Thunderbolt storage, the pattern generalizes: extract the CDB set, wrap it in a minimal user client, push complexity to userspace.
Next Claude Code's concise output mode drops token usage by 40% in my →