Hi all,
We are migrating a SCSI HBA driver from KEXT to DriverKit (DEXT), with our DEXT inheriting from IOUserSCSIParallelInterfaceController. We've encountered a data corruption issue that is reliably reproducible under specific conditions and are hoping for some assistance from the community.
Hardware and Driver Configuration:
Controller: LSI 3108
DEXT Configuration: We are reporting our hardware limitations to the framework via the UserReportHBAConstraints function, with the following key settings:
// UserReportHBAConstraints...
addConstraint(kIOMaximumSegmentAddressableBitCountKey, 0x20); // 32-bit
addConstraint(kIOMaximumSegmentCountWriteKey, 129);
addConstraint(kIOMaximumByteCountWriteKey, 0x80000); // 512KB
Observed Behavior: Direct I/O vs. Buffered I/O
We've observed that the I/O behavior differs drastically depending on whether it goes through the system file cache:
1. Direct I/O (Bypassing System Cache) -> 100% Successful
When we use fio with the direct=1 flag, our read/write and data verification tests pass perfectly for all file sizes, including 20GB+.
2. Buffered I/O (Using System Cache) -> 100% Failure at >128MB
Whether we use the standard cp command or fio with the direct=1 option removed to simulate buffered I/O, we observe the exact same, clear failure threshold:
Test Results:
File sizes ≤ 128MB: Success. Data checksums match perfectly.
File sizes ≥ 256MB: Failure. Checksums do not match, and the destination file is corrupted.
Evidence of failure reproduced with fio (buffered_integrity_test.fio, with direct=1 removed):
fio --size=128M buffered_integrity_test.fio -> Test Succeeded (err=0).
fio --size=256M buffered_integrity_test.fio -> Test Failed (err=92), reporting the following error, which proves a data mismatch during the verification phase:
verify: bad header ... at file ... offset 1048576, length 1048576
fio: ... error=Illegal byte sequence
Our Analysis and Hypothesis
The phenomenon of "Direct I/O succeeding while Buffered I/O fails" suggests the problem may be related to the cache synchronization mechanism at the end of the I/O process:
Our UserProcessParallelTask_Impl function correctly handles READ and WRITE commands.
When cp or fio (buffered) runs, the WRITE commands are successfully written to the LSI 3108 controller's onboard DRAM cache, and success is reported up the stack.
At the end of the operation, to ensure data is flushed to disk, the macOS file system issues an fsync, which is ultimately translated into a SYNCHRONIZE CACHE SCSI command (Opcode 0x35 or 0x91) and sent to our UserProcessParallelTask_Impl.
We hypothesize that our code may not be correctly identifying or handling this SYNCHRONIZE CACHE opcode. It might be reporting "success" up the stack without actually commanding the hardware to flush its cache to the physical disk.
The OS receives this "success" status and assumes the operation is safely complete.
In reality, however, the last batch of data remains only in the controller's volatile DRAM cache and is eventually lost.
This results in an incomplete or incorrect file tail, and while the file size may be correct, the data checksum will inevitably fail.
Summary
Our DEXT driver performs correctly when handling Direct I/O but consistently fails with data corruption when handling Buffered I/O for files larger than 128MB. We can reliably reproduce this issue using fio with the direct=1 option removed.
The root cause is very likely the improper handling of the SYNCHRONIZE CACHE command within our UserProcessParallelTask. P.S. This issue did not exist in the original KEXT version of the driver.
We would appreciate any advice or guidance on this issue.
Thank you.
PCIDriverKit
RSS for tagDevelop device drivers for Peripheral Component Interconnect accessories.
Posts under PCIDriverKit tag
26 Posts
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hi everyone,
We're trying to prepare a DriverKit App for a client test, and we've run into an unavoidable signing conflict that seems to be caused by the Xcode Archive process itself.
Background & Environment:
Environment: macOS 15.6.1, Xcode 16.4
Our project consists of a main App Target and a DEXT Target.
Both the Debug and Release configurations for both targets are set to Xcode's default: Automatically manage signing.
Our developer account holds a valid, active Developer ID Application (With Kext) certificate, which we use for signing our legacy KEXT.
The Action That Triggers Failure:
From this clean state, we execute Product -> Archive.
The Archive process fails during the signing validation phase and presents the following three errors, completely halting the process:
There is a problem with the request entity - You already have a current Developer ID Application Managed (With Kext) certificate...
No profiles for 'com.company.Acxxx.driver' were found...
No profiles for 'com.company.Acxxx.app' were found...
This error seems to indicate that the Xcode Archive process:
Ignores the project's Release configuration (even the default 'Auto' setting).
Attempts to automatically create a new, standard Developer ID certificate for us.
This action conflicts with the existing (With Kext) certificate in our account, causing the entire Archive process to fail.
The "Failed Experiment" to Resolve This:
To work around this automation conflict, we tried the solution: configuring a fully manual signing process for the Release configuration to explicitly tell Xcode to use our existing KEXT certificate.
Our Steps: We disabled automatic signing for both the App and DEXT targets for the Release configuration and manually assigned the Developer ID Provisioning Profiles created for our Developer ID (With Kext) certificate.
The New Problem: After doing this, the Signing Certificate field for the DEXT Target's Signing & Capabilities interface now shows None, accompanied by the misleading warning about needing a DriverKit development profile.
The Outcome: This None issue now prevents us from even starting the Archive process, as the project fails to build due to the incorrect signing configuration. We've tried every debugging step — including rebuilding profiles, validating the keychain, and clearing caches — but nothing resolves this None issue.
Our Dilemma:
State A (Fully Automatic Signing): The Archive process fails due to the KEXT certificate conflict.
State B (Manual Release Signing): The project fails to build due to the Signing Certificate: None issue, preventing us from initiating an Archive.
For a development team holding a KEXT Developer ID certificate, how should an Xcode project be configured when migrating to DriverKit, so that the Archive process:
Does not trigger the flawed automation logic that attempts to create a new certificate?
And, does not fall into the Signing Certificate: None configuration trap?
Related Forum Threads We've Studied:
https://developer.apple.com/forums/thread/781932
https://developer.apple.com/forums/thread/751490
https://developer.apple.com/forums/thread/767152
https://developer.apple.com/forums/thread/721563
Best Reagrds,
Charles
Topic:
Code Signing
SubTopic:
Certificates, Identifiers & Profiles
Tags:
SCSIControllerDriverKit
PCIDriverKit
DriverKit
Hello,
I'm trying to make a DEXT for a thunderbolt device. I started from the DriverKit template that does a very simple Hello World. I added the DriverKit PCI (development) entitlement in the developer portal. The dext is installed and activated but when I connect my thunderbolt device this is what I see in the console log:
kernel DK: mydrv-0x100010a85 waiting for server com.mycompany.mydrvApp.mydrv-100010a85
kernel Driver com.mycompany.mydrvApp.mydrv has crashed 0 time(s)
kernelmanagerd Launching dext com.mycompany.mydrvApp.mydrv com.mycompany.mydrvApp.mydrv 0x100010a81 e675cb5ca6b6650163cc231c6af2f7e730b56b0bf394b857ce76f8e3105eb0f1
kernel DK: mydrv-0x100010a89 waiting for server com.mycompany.mydrvApp.mydrv-100010a89
kernelmanagerd Launching driver extension: Dext com.mycompany.mydrvApp.mydrv v1 in executable dext bundle com.mycompany.mydrvApp.mydrv at /Library/SystemExtensions/DC2F3964-043D-445E-A6CF-A9D7C529B39A/com.mycompany.mydrvApp.mydrv.dext
default 16:52:31.551867-0500 kernel /Library/SystemExtensions/DC2F3964-043D-445E-A6CF-A9D7C529B39A/com.mycompany.mydrvApp.mydrv.dext/com.mycompany.mydrvApp.mydrv[15788] ==> com.apple.dext
kernelmanagerd Found 1 dexts with bundle identifier com.mycompany.mydrvApp.mydrv
kernelmanagerd Using unique id e675cb5ca6b6650163cc231c6af2f7e730b56b0bf394b857ce76f8e3105eb0f1 to pick dext matching bundle identifier com.mycompany.mydrvApp.mydrv
kernelmanagerd Picked matching dext for bundle identifier com.mycompany.mydrvApp.mydrv: Dext com.mycompany.mydrvApp.mydrv v1 in executable dext bundle com.mycompany.mydrvApp.mydrv at /Library/SystemExtensions/DC2F3964-043D-445E-A6CF-A9D7C529B39A/com.mycompany.mydrvApp.mydrv.dext
kernelmanagerd Launching dext com.mycompany.mydrvApp.mydrv com.mycompany.mydrvApp.mydrv 0x100010a85 e675cb5ca6b6650163cc231c6af2f7e730b56b0bf394b857ce76f8e3105eb0f1
kernel DK: com.mycompany.mydrvApp.mydrv[15788] has team identifier L86BQ63GK2
kernelmanagerd Launching driver extension: Dext com.mycompany.mydrvApp.mydrv v1 in executable dext bundle com.mycompany.mydrvApp.mydrv at /Library/SystemExtensions/DC2F3964-043D-445E-A6CF-A9D7C529B39A/com.mycompany.mydrvApp.mydrv.dext
kernel
kernel DK: mydrv-0x100010a81: provider entitlements check failed
kernel DK: IOUserServer(com.mycompany.mydrvApp.mydrv-0x100010a81)-0x100010a8a::exit(Entitlements check failed)
What am I missing for the check to pass?
Here is my mydrv.entitlements file:
I tried adding IOPCIPrimaryMatch with my vendor id in info.plist, but with same result.
Developer mode is on and SIP is disabled.
Thanks
I have a DriverKit system extension (dext) that uses PCIDriverKit. I would like to get the build environment straightened out to successfully distribute the dext and associated software to end users.
There are three types of software involved:
The Dext-hosting application - this is the application that must be installed to /Applications/, and will perform the registration of the dext. The dext is deployed "within" this application, and can be found in the /Contents/Library/SystemExtensions folder of the app bundle.
The dext itself - this is the actual binary system extension, which will be registered by its owning application, and will operate in its own application space independent of the hosting application.
Additional applications that communicate with the dext - these are applications which will connect to the dext through user clients, but these applications do not contain the dext themselves.
There are multiple locations where settings need to be exactly correct for each type of software to be signed, provisioned, and notarized properly in order to be distributed to users:
developer.apple.com - where "identifiers" and "provisioning profiles" are managed. Note that there are differences in access between "Team Agent", "Admin", and "Developer" at this site.
Xcode project's Target "Signing & Capabilities" tab - this is where "automatically manage signing" can be selected, as well as team selection, provisioning profile selection, and capabilities can be modified.
Xcode project's Target "Build Settings" tab - this is where code signing identity, code signing development team, code signing entitlements file selection, Info.plist options and file selection, and provisioning profile selection.
Xcode's Organizer window, which is where you manage archives and select for distribution. In this case, I am interested in "Developer ID" Direct Distribution - I want the software signed with our company's credentials (Team Developer ID) so that users know they can trust the software.
Choosing "automatically manage signing" does not work for deployment. The debug versions of software include DriverKit (development) capability (under App ID configuration at developer.apple.com), and this apparently must not be present in distributable provisioning. I believe this means that different provisioning needs to occur between debug and release builds?
I have tried many iterations of selections at all the locations, for all three types of binaries, and rather than post everything that does not work, I am asking, "what is supposed to work?"
Hello everyone,
We are migrating a KEXT storage driver to DriverKit. In our KEXT, we use a "one LUN = one Target" model and successfully create multiple targets in a loop during initialization. We are now trying to replicate this architecture in our DEXT.
The issue is that only Target 0 is fully probed and mounted. For Target 1, the lifecycle silently stops after the first TEST UNIT READY command is successfully acknowledged. The macOS SCSI layer never sends any subsequent probe commands (like INQUIRY) to this target.
The failure sequence for Target 1, observed from our logs (regardless of whether Target 0 is created), is as follows:
AsyncCreateTargetForID(1) -> UserInitializeTargetForID(1) (Succeeds)
UserProcessParallelTask(Target: 1, Opcode: TUR) (Succeeds)
The DEXT correctly acknowledges the TUR command for Target 1 by returning kSCSITaskStatus_CHECK_CONDITION with UNIT ATTENTION in the Sense Data (Succeeds)
<-- Breakpoint -->
UserProcessParallelTask(Target: 1, Opcode: INQUIRY) (Never happens)
Through log comparison, we have confirmed that the DEXT's response to the TUR command for Target 1 is identical to the successful KEXT's response.
We have tried creating only Target 1 (skipping Target 0 entirely), but the behavior is exactly the same -> the probe still stalls after the TUR.
We initially suspected a race condition caused by consecutive calls to AsyncCreateTargetForID(). We attempted several methods to ensure that targets are created sequentially, such as trying to build a "creation chain" using OSAction completion handlers. However, these attempts were unsuccessful due to various compilation errors and API misunderstandings.
In any case, this "race condition" theory was ultimately disproven by our experiment where creating only Target 1 still resulted in failure.
We would like to ask two questions:
Is our inability to have a Target ID greater than 0 fully probed by macOS a bug in our own code, or could there be another reason we are unaware of?
If we do indeed need a "one-after-another" creation mechanism for AsyncCreateTargetForID, what is the correct way to implement a "chained creation" using OSAction completion handlers in DriverKit?
Thank you for any help or guidance.
Best Regards,
Charles
Hello everyone,
We are in the process of migrating a high-performance storage KEXT to DriverKit. During our initial validation phase, we noticed a performance gap between the DEXT and the KEXT, which prompted us to try and optimize our I/O handling process.
Background and Motivation:
Our test hardware is a RAID 0 array of two HDDs. According to AJA System Test, our legacy KEXT achieves a write speed of about 645 MB/s on this hardware, whereas the new DEXT reaches about 565 MB/s. We suspect the primary reason for this performance gap might be that the DEXT, by default, uses a serial work-loop to submit I/O commands, which fails to fully leverage the parallelism of the hardware array.
Therefore, to eliminate this bottleneck and improve performance, we configured a dedicated parallel dispatch queue (MyParallelIOQueue) for the UserProcessParallelTask method.
However, during our implementation attempt, we encountered a critical issue that caused a system-wide crash.
The Operation Causing the Panic:
We configured MyParallelIOQueue using the following combination of methods:
In the .iig file: We appended the QUEUENAME(MyParallelIOQueue) macro after the override keyword of the UserProcessParallelTask method declaration.
In the .cpp file: We manually created a queue with the same name by calling the IODispatchQueue::Create() function within our UserInitializeController method.
The Result:
This results in a macOS kernel panic during the DEXT loading process, forcing the user to perform a hard reboot.
After the reboot, checking with the systemextensionsctl list command reveals the DEXT's status as [activated waiting for user], which indicates that it encountered an unrecoverable, fatal error during its initialization.
Key Code Snippets to Reproduce the Panic:
In .iig file - this was our exact implementation:
class DRV_MAIN_CLASS_NAME: public IOUserSCSIParallelInterfaceController
{
public:
virtual kern_return_t UserProcessParallelTask(...) override
QUEUENAME(MyParallelIOQueue);
};
In .h file:
struct DRV_MAIN_CLASS_NAME_IVars {
// ...
IODispatchQueue* MyParallelIOQueue;
};
In UserInitializeController implementation:
kern_return_t
IMPL(DRV_MAIN_CLASS_NAME, UserInitializeController)
{
// ...
// We also included code to manually create the queue.
kern_return_t ret = IODispatchQueue::Create("MyParallelIOQueue",
kIODispatchQueueReentrant,
0,
&ivars->MyParallelIOQueue);
if (ret != kIOReturnSuccess) {
// ... error handling ...
}
// ...
return kIOReturnSuccess;
}
Our Question:
What is the officially recommended and most stable method for configuring UserProcessParallelTask_Impl() to use a parallel I/O queue?
Clarifying this is crucial for all developers pursuing high-performance storage solutions with DriverKit. Any explanation or guidance would be greatly appreciated.
Best Regards,
Charles
Hi everyone,
We are in the process of migrating a legacy KEXT for our external multi-disk RAID enclosure to the modern DriverKit framework. During the performance validation of our KEXT, we observed a large and consistent maximum throughput difference between Intel-based Macs and Apple Silicon-based Macs. We would like to share our findings and hope to discuss with others in the community to see if you have had similar experiences that could confirm or correct our understanding.
The Observation: A Consistent Performance Gap
When using the exact same external RAID hardware (an 8-HDD RAID 5 array), driven by our mature KEXT, we see the following results in high-throughput benchmarks (AJA System Test, large sequential writes):
On a 2020 Intel-based Mac: We consistently achieve a throughput of ~2500 MB/s.
On modern M-series Macs (from M1 to M4): The throughput is consistently capped at ~1500 MB/s.
This performance difference of nearly 40% is significant and is present across the entire Apple Silicon product line.
Our Hypothesis: A Shift in Architectural Design Philosophy
Since the KEXT and external hardware are identical in both tests, we believe this performance difference is not a bug but a fundamental platform architecture distinction. Our hypothesis is as follows:
1. The Intel Mac Era ("Dedicated Throughput")
The Intel-based Macs we tested use a dedicated, discrete Intel Thunderbolt controller chip. This chip has its own dedicated PCIe lanes and resources, and its design appears to be singularly focused on maximizing raw, sustained data throughput for external peripherals.
2. The Apple Silicon Era ("Integrated Efficiency")
In contrast, M-series Macs use a deeply integrated I/O controller inside the SoC. This controller must share resources, such as the total unified memory bandwidth and the chip's overall power budget, with all other functional units (CPU, GPU, etc.).
We speculate that the design priority for this integrated I/O controller has shifted from "maximizing single-task raw throughput" to "maximizing overall system efficiency, multi-task responsiveness, and low latency." As a result, in a pure, single-task storage benchmark, its performance ceiling may be lower than that of the older, dedicated-chip architecture.
Our Question to the Community:
Is our understanding correct? Have other developers of high-performance storage drivers or peripherals also observed a similar performance ceiling for external storage on Apple Silicon Macs, when compared to high-end Intel Macs?
We believe that understanding this as a deliberate architectural trade-off is crucial for setting realistic performance targets for our DEXT. Our current goal has been adjusted to have our DEXT match the KEXT's ~1500 MB/s on the M-series platform.
Any insights, confirmations, or corrections from the community or Apple engineers would be greatly appreciated.
Thank you very much!
Charles
Hello everyone,
We are migrating our KEXT for a Thunderbolt storage device to a DEXT based on IOUserSCSIParallelInterfaceController.
We've run into a fundamental issue where the driver's behavior splits based on the I/O source: high-level I/O from the file system (e.g., Finder, cp) is mostly functional (with a minor ls -al sorting issue for Traditional Chinese filenames), while low-level I/O directly to the block device (e.g., diskutil) fails or acts unreliably. Basic read/write with dd appears to be mostly functional.
We suspect that our DEXT is failing to correctly register its full device "personality" with the I/O Kit framework, unlike its KEXT counterpart. As a result, low-level I/O requests with special attributes (like cache synchronization) sent by diskutil are not being handled correctly by the IOUserSCSIParallelInterfaceController framework of our DEXT.
Actions Performed & Relevant Logs
1. Discrepancy: diskutil info Shows Different Device Identities for DEXT vs. KEXT
For the exact same hardware, the KEXT and DEXT are identified by the system as two different protocols.
KEXT Environment:
Device Identifier: disk5
Protocol: Fibre Channel Interface
...
Disk Size: 66.0 TB
Device Block Size: 512 Bytes
DEXT Environment:
Device Identifier: disk5
Protocol: SCSI
SCSI Domain ID: 2
SCSI Target ID: 0
...
Disk Size: 66.0 TB
Device Block Size: 512 Bytes
2. Divergent I/O Behavior: Partial Success with Finder/cp vs. Failure with diskutil
High-Level I/O (Partially Successful):
In the DEXT environment, if we operate on an existing volume (e.g., /Volumes/MyVolume), file copy operations using Finder or cp succeed. Furthermore, the logs we've placed in our single I/O entry point, UserProcessParallelTask_Impl, are triggered.
Side Effect: However, running ls -al on such a volume shows an incorrect sorting order for files with Traditional Chinese names (they appear before . and ..).
Low-Level I/O (Contradictory Behavior):
In the DEXT environment, when we operate directly on the raw block device (/dev/disk5):
diskutil partitionDisk ... -> Fails 100% of the time with the error: Error: -69825: Wiping volume data to prevent future accidental probing failed.
dd command -> Basic read/write operations appear to work correctly (a write can be immediately followed by a read within the same DEXT session, and the data is correct).
3. Evidence of Cache Synchronization Failure (Non-deterministic Behavior)
The success of the dd command is not deterministic. Cross-environment tests prove that its write operations are unreliable:
First Test:
In the DEXT environment, write a file with random data to /dev/disk5 using dd.
Reboot into the KEXT environment.
Read the data back from /dev/disk5 using dd. The result is a file filled with all zeros.
Conclusion: The write operation only went to the hardware cache, and the data was lost upon reboot.
Second Test:
In the DEXT environment, write the same random file to /dev/disk5 using dd.
Key Variable: Immediately after, still within the DEXT environment, read the data back once for verification. The content is correct!
Reboot into the KEXT environment.
Read the data back from /dev/disk5. This time, the content is correct!
Conclusion: The additional read operation in the second test unintentionally triggered a hardware cache flush. This proves that the dd (in our DEXT) write operation by itself does not guarantee synchronization, making its behavior unreliable.
Our Problem
Based on the observations above, we have the conclusion:
High-Level Path (triggered by Finder/cp):
When an I/O request originates from the high-level file system, the framework seems to enter a fully-featured mode. In this mode, all SCSI commands, including READ/WRITE, INQUIRY, and SYNCHRONIZE CACHE, are correctly packaged and dispatched to our UserProcessParallelTask_Impl entry point. Therefore, Finder operations are mostly functional.
Low-Level Path (triggered by dd/diskutil):
When an I/O request originates from the low-level raw block device layer:
The most basic READ/WRITE commands can be dispatched (which is why dd appears to work).
However, critical management commands, such as INQUIRY and SYNCHRONIZE CACHE, are not being correctly dispatched or handled. This leads to the incorrect device identification in diskutil info and the failure of diskutil partitionDisk due to its inability to confirm cache synchronization.
We would greatly appreciate any guidance, suggestions, or insights on how to resolve this discrepancy. Specifically, what is the recommended approach within DriverKit to ensure that a DEXT based on IOUserSCSIParallelInterfaceController can properly declare its capabilities and handle both high-level and low-level I/O requests uniformly?
Thank you.
Charles
Hello everyone,
I am migrating a KEXT for a SCSI PCI RAID controller (LSI 3108 RoC) to DriverKit (DEXT). While the DEXT loads successfully, I'm facing a DMA issue: an INQUIRY command results in a 0-byte disk because the data buffer received by the DEXT is all zeros, despite our firmware logs confirming that the correct data was prepared and sent.
We have gathered detailed forensic evidence and would appreciate any insights from the community.
Detailed Trace of a Failing INQUIRY Command:
1, DEXT Dispatches the Command:
Our UserProcessParallelTask implementation correctly receives the INQUIRY task. Logs show the requested transfer size is 6 bytes, and the DEXT obtains the IOVA (0x801c0000) to pass to the hardware.
DEXT Log:
[UserProcessParallelTask_Impl] --- FORENSIC ANALYSIS ---
[UserProcessParallelTask_Impl] fBufferIOVMAddr = 0x801c0000
[UserProcessParallelTask_Impl] fRequestedTransferCount = 6
2, Firmware Receives IOVA and Prepares Correct Data:
A probe in our firmware confirms that the hardware successfully received the correct IOVA and the 6-byte length requirement. The firmware then prepares the correct 6-byte INQUIRY response in its internal staging buffer.
Firmware Logs:
-- [FIRMWARE PROBE: INCOMING DMA DUMP] --
Host IOVA (High:Low) = 0x00000000801c0000
DataLength in Header = 6 (0x6)
--- [Firmware Outgoing Data Dump from go_inquiry] ---
Source Address: 0x228BB800, Length: 6 bytes
0x0000: 00 00 05 12 1F 00
3, Hardware Reports a Successful Transfer, but Data is Lost:
After the firmware initiates the DMA write to the Host IOVA, the hardware reports a successful transfer of 6 bytes back to our DEXT.
DEXT Completion Log:
[AME_Host_Normal_Handler_SCSI_Request] [TaskID: 200] COMPLETING...
[AME_Host_Normal_Handler_SCSI_Request] Hardware Transferred = 6 bytes
[AME_Host_Normal_Handler_SCSI_Request] - ReplyStatus = SUCCESS (0x0)
[AME_Host_Normal_Handler_SCSI_Request] - SCSIStatus = SUCCESS (0x0)
The Core Contradiction:
Despite the firmware preparing the correct data and the hardware reporting a successful DMA transfer, the fDataBuffer in our DEXT remains filled with zeros. The 6 bytes of data are lost somewhere between the PCIe bus and host memory.
This "data-in-firmware, zeros-in-DEXT" phenomenon leads us to believe the issue lies in memory address translation or a system security policy, as our legacy KEXT works perfectly on the same hardware.
Compared to a KEXT, are there any known, stricter IOMMU/security policies for a DEXT that could cause this kind of "silent write failure" (even with a correct IOVA)?
Alternatively, what is the correct and complete expected workflow in DriverKit for preparing an IOMemoryDescriptor* fDataBuffer (received in UserProcessParallelTask) for a PCI hardware device to use as a DMA write target?
Any official documentation, examples, or advice on the IOMemoryDescriptor to PCI Bus Address workflow would be immensely helpful.
Thank you.
Charles
We've developed a PCIDriverKit driver for the capture card on macOS and have identified an issue: CreateMemoryDescriptorFromClient can only read data from the user space to the driver, but cannot write data back to the user.
typedef struct _MWCAP_EDID_DATA {
uint64_t size;
uint64_t uaddr;
} MWCAP_EDID_DATA;
// App
size_t xxx::GetEdid(void *buff, size_t size)
{
MWCAP_EDID_DATA edid;
edid.size = size;
edid.uaddr = (uint64_t)buff;
kr = IOConnectCallStructMethod(
connect,
kUserGetEdid,
&edid,
sizeof(MWCAP_EDID_DATA),
NULL,
NULL
);
// kr is 0. But However, the data in the buffer remains unchanged;
// it does not reflect the EDID copied from the DEXT.
return size;
}
// Driver
MWCAP_EDID_DATA *edid = (MWCAP_EDID_DATA *)input;
IOMemoryDescriptor *user_buf_mem = NULL;
IOAddressSegment segment;
segment.address = edid->uaddr;
segment.length = edid->size;
// We have verified that the values in edid->uaddr and edid->size are consistent with what was set by the application.
ret = CreateMemoryDescriptorFromClient(kIOMemoryDirectionOutIn, 1, &segment, &user_buf_mem);
if (ret != kIOReturnSuccess) {
os_log(OS_LOG_DEFAULT, "Failed to create memdesc with error: 0x%08x", ret);
break;
}
IOMemoryMap* user_buf_map = nullptr;
ret = user_buf_mem->CreateMapping(0, 0, 0, 0, 0, &user_buf_map);
if (ret != kIOReturnSuccess) {
os_log(OS_LOG_DEFAULT, "Failed to create mapping with error: 0x%08x", ret);
OSSafeReleaseNULL(user_buf_mem);
break;
}
// ... fill the user_buf_map with edid data ...
// For example:
// memcpy(user_buf_map->GetAddress(), source_edid_data, edid->size);
// At this point we have also verified the data in user_buf_map->GetAddress(), which matches our expectations.
OSSafeReleaseNULL(user_buf_map);
OSSafeReleaseNULL(user_buf_mem);
Please help take a look, thank you!
We have developed the driver for the ProCapture video capture card based on PCIDriverKit. The App can communicate with the driver through the UserClient API. Currently, there is an issue where, when the App starts, there is a small probability that it causes a driver crash. However, the crash stack trace does not point to our code but appears to be within the PCIDriverKit framework. We have spent several weeks debugging but still cannot identify the root cause of the crash. Could you please review the crash log and suggest any methods to help pinpoint the issue?
com.magewell.ProCaptureDriver-2025-09-15-153522.ips
com.magewell.ProCaptureDriver-2025-09-15-082500.ips
I'm trying to help out one of our vendors by building a skeleton PCI dext which they can flesh out.
However, I can't seem to get the signing right.
I can't sign it at all using no team or my personal team. "Signing for requires a development team", and "Personal development teams ... do not support the System Extension capability".
I can't sign the driver because "DriverKit Team Provisioning Profile: doesn't match the entitlements file's value for the com.apple.developer.driverkit.transport.pci entitlement.
I think this problem occurs because our company has already been assigned a transport.pci entitlement, but for our own PCI vendor ID. But I want to build and test software that works with our vendor's PCI device.
I tried generating a profile for the driver manually, it contained only our own company's PCI driver match:
IOPCIPrimaryMatch = "0x0000MMMM&0x0000FFFF";
where MMMM is our own PCI vendor ID.
Is there a better way to inspect the profile Xcode is using than the postage-stamped sized info popup which truncates the information? I would download the generated profile but it doesn't appear on the profile, but Xcode is accessing it from somewhere.
When I look at the available capabilities I can add to an app identifier on the Developer portal, I see com.apple.developer.driverkit.transport.usb, which is "development only". There's no "development only" capability for PCI. Does this mean it isn't possible to develop even a proof-of-concept PCI driver without being first granted the DriverKit PCI (Primary Match) entitlement?
When adding capabilities to a driver, the list of available capabilities shown in Xcode has one "DriverKit PCI (Primary Match) entry", but if I double click it, two such entries appear in the Signing and Capabilities tab for my driver target. On the Developer portal, when I look at my driver's Identifier, there are two Capabilities labelled DriverKit PCI (Primary Match). Why?
We have a management application which manages security enable and disable for an external PCIe based storage device using Kernel Extension(SCSI Architecture Model Family for External USB Mass Storage devices which deals with IOSCSIBlockCommandsDevice, IOSCSIPeripheralDeviceType00 and IOBlockStorageService)
Now the issue we are facing is when we try to unlock the device using security code (already being set) , we will relink the device using VU command (RelinkBridge). Even-though both the commands are successful , the device is not getting relinked due to which we are not able to use the device for storage.
If someone have faced similar issue or any pointer on how to resolve this issue , it would be helpful.
Thanks in advance.
Hello everyone,
I’m working on implementing hardware interrupt handling in DriverKit and came across the InterruptOccurred method in IOInterruptDispatchSource. I noticed that its declaration ends with a TYPE macro:
virtual void InterruptOccurred(OSAction* action, uint64_t count, uint64_t time)
TYPE(IOInterruptDispatchSource::InterruptOccurred);
This structure seems similar to how Timer Events are set up, where an event is linked to a callback and triggered by a timer. I’m attempting to use a similar approach, but for hardware-triggered interrupts rather than timer events.
I’m currently in the trial-and-error phase of the implementation, but if anyone has a working example or reference on how to properly implement and register InterruptOccurred, it would be greatly appreciated!
Best regards,
Charles
Hi everyone,
I was following the Video Modernize PCI and SCSI drivers with DriverKit and the Document to implement UserMapHBAData(), and here’s my current implementation:
// kern_return_t DRV_MAIN_CLASS_NAME::UserMapHBAData_Impl(uint32_t *uniqueTaskID)
kern_return_t IMPL(DRV_MAIN_CLASS_NAME, UserMapHBAData)
{
Log("UserMapHBAData() - Start");
// Define the vm_page_size explicitly
const uint32_t vm_page_size = 4096;
kern_return_t ret;
IOBufferMemoryDescriptor *buffer = nullptr;
IOMemoryMap *memMap = nullptr;
void *taskData = nullptr;
// Create a buffer for HBA-specific task data
ret = IOBufferMemoryDescriptor::Create(kIOMemoryDirectionOutIn, ivars->fTaskDataSize, vm_page_size, &buffer);
__Require((kIOReturnSuccess == ret), Exit);
// Map memory to the driver extension's memory space
ret = buffer->CreateMapping(0, 0, 0, 0, 0, &memMap);
__Require((kIOReturnSuccess == ret), Exit);
// Retrieve mapped memory address
taskData = reinterpret_cast<void *>(memMap->GetAddress());
__Require(taskData, Exit);
// WARNING: Potential leak of an object stored into 'buffer'
// WARNING: Potential leak of an object stored into 'memMap'
// Assign a unique task ID
ivars->fTaskID++; // ERROR: No member named 'fTaskID' in 'DriverKitAcxxx_IVars'
ivars->fTaskArray[ivars->fTaskID] = taskData;
*uniqueTaskID = ivars->fTaskID;
Log("UserMapHBAData() - End");
return kIOReturnSuccess;
Exit:
// Cleanup in case of failure
if (memMap) {
memMap->free(); // Correct method for releasing memory maps
}
if (buffer) {
buffer->free(); // Correct method for releasing buffer memory
}
LogErr("ret = 0x%0x", ret);
Log("UserMapHBAData() - End");
return ret;
}
For reference, in KEXT, memory allocation is typically done using:
IOBufferMemoryDescriptor *buffer = IOBufferMemoryDescriptor::inTaskWithOptions(
kernel_task, // Task in which memory is allocated
kIODirectionOutIn, // Direction (read/write)
1024, // Size of the buffer in bytes
4); // Alignment requirements
However, after installing the dext, macOS hangs, and I have to do a hardware reset. After rebooting, the sysctl list output shows:
% sectl list
1 extension(s)
--- com.apple.system_extension.driver_extension
enabled active teamID bundleID (version) name [state]
* - com.accusys.DriverKitAcxxx (5.0/11) com.accusys.DriverKitAcxxx [activated waiting for user]
Questions:
What could be causing macOS to halt?
How should I approach debugging and resolving this issue?
Looking forward to your insights, any suggestions would be greatly appreciated!
Best regards, Charles
Hello all,
I am interested in developing a small driver that would facilitate host-to-host communication via Thunderbolt 4/5. While I am aware of features such as Thunderbolt Bridge/Thunderbolt Networking, I find that for my application the overhead is too great. I am interested in sharing a simple, static memory buffer between the two hosts for IO and with some synchronisation primitives. The idea being that the communication is facilitated between different platforms.
Would it be possible to develop a driver/service like this? Currently, going through the documentation, to use PCIDriverKit specifying a Vendor and Product Ids is required, so I doubt that this is a viable path.
I know that Linux exposes the "XDomain" protocol to announce thunderbolt services (This is the same protocol that is used in macOS to discover Thunderbolt Networking peers). Is this functionality exposed to macOS driver developers?
Hello everyone
I have been developing PCIe device driver through Thunderbolt. However, it was confirmed that up to three devices connected to the daisy chain worked normally, but the fourth device failed to operate the _CopyDeviceMemoryWithIndex() function for connection with the BAR0 App and did not work properly.
The standard specification of Thunderbolt 3/4 is said to be supported by daisy chain connection up to 6 units, but in reality, it is only 3 units, so I ask the forum for technical confirmation.
Of course total 4 device by 2-port x 2-device daisy chain connecting has working well.
And the PCI entry in System information app indicates that all devices have normal load of the PCIe device driver.
Hello everyone.
I have been developing PCIe device driver through Thunderbolt.
However, it was confirmed that up to three devices connected to the daisy chain worked normally,
but the fourth device failed to operate the _CopyDeviceMemoryWithIndex() function for connection with the BAR0 App and did not work properly.
The standard specification of Thunderbolt 3/4 is said to be supported by daisy chain connection up to 6-device,
but in reality, it is only 3 units,
so I ask the forum for technical confirmation.
Of course total 4 device by 2-port x 2-device daisy chain connecting has working well.
The PCI entry in System information indicates that all devices have normal load of the PCIe device driver.
Thank you.
Hello Everyone,
I'm encountering an issue while setting up a timer event in DriverKit and would appreciate any guidance.
Here's my current implementation:
void DRV_MAIN_CLASS_NAME::SetupEventTimer()
{
// 1. Create dispatch queue
kern_return_t ret = IODispatchQueue::Create("TimerQueue", 0, 0, &ivars->dispatchQueue);
if (ret != kIOReturnSuccess) {
LogErr("Failed to create dispatch queue: 0x%x", ret);
return;
}
// 2. Create timer source
ret = IOTimerDispatchSource::Create(ivars->dispatchQueue, &ivars->dispatchSource);
if (ret != kIOReturnSuccess) {
LogErr("Failed to create timer: 0x%x", ret);
OSSafeReleaseNULL(ivars->dispatchQueue);
return;
}
/*!
* @brief Create an instance of OSAction.
* @discussion Methods to allocate an OSAction instance are generated for each method defined in a class with
* a TYPE attribute, so there should not be any need to directly call OSAction::Create().
* @param target OSObject to receive the callback. This object will be retained until the OSAction is
* canceled or freed.
* @param targetmsgid Generated message ID for the target method.
* @param msgid Generated message ID for the method invoked by the receiver of the OSAction
* to generate the callback.
* @param referenceSize Size of additional state structure available to the creator of the OSAction
* with GetReference.
* @param action Created OSAction with +1 retain count to be released by the caller.
* @return kIOReturnSuccess on success. See IOReturn.h for error codes.
*/
// 3: Create an OSAction for the TimerOccurred method
// THIS IS WHERE I NEED HELP
OSAction* timerAction = nullptr;
ret = OSAction::Create(this, 0, 0, 0, &timerAction);
if (ret != kIOReturnSuccess) {
LogErr("Failed to create OSAction: 0x%x", ret);
goto cleanup;
}
// 4. Set handler
ret = ivars->dispatchSource->SetHandler(timerAction);
if (ret != kIOReturnSuccess) {
LogErr("Failed to set handler: 0x%x", ret);
goto cleanup;
}
// 5. Schedule timer (1 second)
uint64_t deadline = mach_absolute_time() + NSEC_PER_SEC;
ivars->dispatchSource->WakeAtTime(0, deadline, 0);
cleanup:
if (ret != kIOReturnSuccess) {
OSSafeReleaseNULL(timerAction);
OSSafeReleaseNULL(ivars->dispatchSource);
OSSafeReleaseNULL(ivars->dispatchQueue);
}
}
Problem:
The code runs but the OSAction callback binding seems incorrect (Step 3).
According to the OSAction documentation, I need to use the TYPE macro to properly bind the callback method. But I try to use
TYPE(DRV_MAIN_CLASS_NAME::TimerOccurred)
kern_return_t TimerOccurred() LOCALONLY;
TYPE(TimerOccurred)
kern_return_t TimerOccurred() LOCALONLY;
kern_return_t TimerOccurred() TYPE(DRV_MAIN_CLASS_NAME::TimerOccurred) LOCALONLY;
All results in Out-of-line definition of 'TimerOccurred' does not match any declaration in 'DRV_MAIN_CLASS_NAME'
Questions:
What is the correct way to declare a timer callback method using TYPE?
How to get the values targetmsgid & msgid generated by Xcode?
Any help would be greatly appreciated!
Best Regards, Charles
Hello Everyone,
I encountered an issue with PCI memory access in DriverKit. In my case, BAR0 is not available, but BAR1 is ready for use. Here’s the log output:
!!! ERROR : Failed to get BAR0 info (error: 0xe00002f0). !!!
BAR1 - MemoryIndex: 0x00000000, Size: 0x00040000, Type: 0
Issue Description
When I initially wrote to BAR0 using memoryIndex = 0, it worked successfully:
AME_Address_Write_32(pAMEData, pAMEData->memoryIndex, AME_HOST_INT_MASK_REGISTER, 0x0F);
However, I mistakenly forgot to update memoryIndex to 1 for BAR1. Surprisingly, the write operation still succeeded.
When I fixed memoryIndex = 1 for BAR1, the write operation no longer had any effect. There was no error, but the expected behavior did not occur.
Relevant API (From IOPCIDevice.iig)
/*!
/*!
* @brief Writes a 32-bit value to the PCI device's aperture at a given memory index.
* @discussion This method writes a 32-bit register on the device and returns its value.
* @param memoryIndex An index into the array of ranges assigned to the device.
* @param offset An offset into the device's memory specified by the index.
* @param data A 32-bit value to be written in host byte order.
*/
void
MemoryWrite32(uint8_t memoryIndex,
uint64_t offset,
uint32_t data) LOCALONLY;
Log Output:
Writes to BAR0 (memoryIndex = 0)
AME_Address_Write_32() called
memoryIndex: 0, offset: 0x34, data: 0xf
Wrote data 0xF to offset 52
AME_Address_Write_32() called
memoryIndex: 0, offset: 0xa0, data: 0x1
Wrote data 0x1 to offset 160
AME_Address_Write_32() called
memoryIndex: 0, offset: 0x20, data: 0xffffffff
Wrote data 0xFFFFFFFF to offset 32
Writes to BAR1 (memoryIndex = 1) – No Response
AME_Address_Write_32() called
memoryIndex: 1, offset: 0x34, data: 0xf
No confirmation log, no visible effect.
Questions
What should memoryIndex be set to for BAR1?
The log shows "BAR1 - MemoryIndex: 0x00000000", but should I be using 1 instead?
How can I verify if a write operation to BAR1 is successful?
Is there a way to check if the memory region is actually writable?
Should I use MemoryRead32() to confirm the written value?
Any guidance would be greatly appreciated!
Best Regards,
Charles