Drivers

Write a kext

Build a minimal IOKit kernel extension — enough to load, register with the registry, and log from the kernel.

A kext (kernel extension) is a loadable bundle of kernel code. On darwinOS, kexts are the primary way drivers hook into IOKit. This guide walks through a no-op kext that loads, logs a banner, and registers itself as a service. It’s the smallest IOKit driver you can write that actually does something.

Before you start, make sure you’ve read the Architecture overview — specifically the IOKit section — and have a built darwinOS to test against.

Skeleton

Create a directory my-kext/ anywhere under darwinos/drivers/contrib/. The build system auto-discovers contributed drivers there on the next make world.

my-kext/
├── Info.plist
├── my_kext.cpp
├── my_kext.hpp
└── Makefile

Info.plist

The plist advertises the kext to IOKit and tells the loader what class to instantiate and when.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleIdentifier</key>
  <string>io.darwinos.contrib.MyKext</string>

  <key>IOKitPersonalities</key>
  <dict>
    <key>MyKext</key>
    <dict>
      <key>CFBundleIdentifier</key>
      <string>io.darwinos.contrib.MyKext</string>
      <key>IOClass</key>
      <string>MyKext</string>
      <key>IOProviderClass</key>
      <string>IOResources</string>
      <key>IOMatchCategory</key>
      <string>MyKextMatchCategory</string>
    </dict>
  </dict>

  <key>OSBundleLibraries</key>
  <dict>
    <key>com.apple.kpi.iokit</key>
    <string>17.0</string>
    <key>com.apple.kpi.libkern</key>
    <string>17.0</string>
  </dict>
</dict>
</plist>

IOProviderClass: IOResources means “match against the root nub” — i.e. load unconditionally, not in response to hardware discovery. Real drivers would match against a specific provider (e.g. AppleARMIO).

my_kext.hpp

#pragma once
#include <IOKit/IOService.h>

class MyKext : public IOService {
    OSDeclareDefaultStructors(MyKext);

public:
    bool start(IOService *provider) override;
    void stop(IOService *provider) override;
};

my_kext.cpp

#include "my_kext.hpp"
#include <IOKit/IOLib.h>

OSDefineMetaClassAndStructors(MyKext, IOService);

bool MyKext::start(IOService *provider) {
    if (!IOService::start(provider)) {
        return false;
    }
    IOLog("MyKext: hello from kernel space\n");
    registerService();
    return true;
}

void MyKext::stop(IOService *provider) {
    IOLog("MyKext: stopping\n");
    IOService::stop(provider);
}

IOLog writes to the kernel’s ring buffer, which surfaces on the console and in dmesg. registerService() makes the driver visible to userspace via the IOKit registry.

Makefile

KEXT_NAME := MyKext
KEXT_BUNDLE_ID := io.darwinos.contrib.MyKext
KEXT_SOURCES := my_kext.cpp

include $(DARWINOS_TOP)/glue/mk/kext.mk

The included fragment handles everything else — darwinos-clang invocation, Embedded C++ flags, the bundle layout, linking against the kernel’s KPI stubs.

Build it

From the repo root:

make kexts

The output lands in build/<target>/kexts/MyKext.kext/. Copy that directory into the guest’s /Library/Extensions/ (same pattern as copying a userland binary).

Load it

In the guest:

kextload /Library/Extensions/MyKext.kext
dmesg | tail
# ... MyKext: hello from kernel space

Unload with:

kextunload -b io.darwinos.contrib.MyKext

What to do next

  • Replace IOProviderClass: IOResources with a real provider so your driver only loads against specific hardware.
  • Match against an IOKit family — IOEthernetController, IOBlockStorageDriver, IOUSBHostInterface — and override the family’s abstract methods.
  • Add a user-client class to allow userland to talk to your driver over Mach messages.

The IOKit reference covers families in detail. When something goes wrong, debug the kernel — set a breakpoint on your start() and step in.