Hi Kevin,
Thank you for your valuable insights. Following your advice, we have refactored our driver to use a "wrapper" pattern where UserProcessBundledParallelTasks serves as a high-performance entry point that forwards commands to our core dispatch logic.
To eliminate potential race conditions, we have also moved our interrupt handling to the Default Dispatch Queue using kIOServiceDefaultQueueName. This ensures that command submission and completion are strictly serialized.
Here are the key implementation details showing how we unified the dispatch logic:
1. Legacy Entry Point (Single Task):
We extracted our core logic into a helper method, DispatchTaskInternal, passing 0xFFFF as a placeholder for the Slot Index.
kern_return_t MyDriver::UserProcessParallelTask_Impl(
SCSIUserParallelTask parallelRequest,
uint32_t *response,
OSAction *completion)
{
// Forward to unified internal dispatcher with no slot index (Legacy Mode)
return DispatchTaskInternal(parallelRequest, response, completion, 0xFFFF);
}
2. Bundled Submission Path:
In our UserProcessBundledParallelTasks implementation, we iterate through the indices, create a local object copy of the request (as you suggested), and call the same internal dispatcher.
void MyDriver::UserProcessBundledParallelTasks_Impl(
const uint16_t parallelRequestSlotIndices[32],
uint16_t parallelRequestSlotIndicesCount,
OSAction * completion)
{
for (uint16_t i = 0; i < parallelRequestSlotIndicesCount; i++) {
uint16_t slotIndex = parallelRequestSlotIndices[i];
const SCSIUserParallelTask& sharedReq = ivars->fCommandBuffers[slotIndex];
// Create a local copy to utilize existing processing logic
SCSIUserParallelTask localReq = sharedReq;
uint32_t response;
// DispatchTaskInternal performs a completion->retain() for EVERY command,
// ensuring the action stays alive regardless of the bundled count.
DispatchTaskInternal(localReq, &response, completion, slotIndex);
}
}
3. Unified Asynchronous Completion Path:
Regardless of the mode, we now use the stable ParallelTaskCompletion within the interrupt handler (ISR), followed by the corresponding release().
// Inside the Asynchronous Interrupt Handler (serialized on Default Queue)
SCSIUserParallelResponse taskResponse = {};
// ... Populate status, TaskID, and Version ...
// Signal completion to the framework
ParallelTaskCompletion(completion, taskResponse);
// Balance the reference count performed during submission
completion->release();
Observations and Current Bottleneck:
If we intercept a command (like TEST UNIT READY) and call the completion logic "synchronously" within the UserProcessBundledParallelTasks scope, the system works perfectly and immediately issues the next command (e.g., INQUIRY).
As soon as the completion is triggered from the asynchronous interrupt path—even though it is serialized on the same Default Queue—invoking ParallelTaskCompletion (or BundledParallelTaskCompletion) causes the DEXT to crash immediately (Corpse / Address size fault).
Questions:
Since we have ensured an independent retain/release cycle for every command and used a serialized queue, why does the OSAction handle appear to become invalid once it enters an asynchronous callback context in Bundled mode? Is there a hidden thread-context or lifecycle restriction on the OSAction provided during a bundled call that differs from legacy mode?
We have verified that the legacy mode remains 100% stable under the same serialized queue configuration. We would appreciate any further guidance on why the "Sync succeeds, Async crashes" behavior persists in the high-performance path.
Best Regards,
Charles
Topic:
App & System Services
SubTopic:
Drivers
Tags: