The Device Model

Roughly 70% of the Linux tree is drivers. They are not seventy thousand independent programs — they are seventy thousand implementations of one small set of interfaces, held together by a framework that decides which driver owns which hardware, calls it when the hardware appears, and cleans up when it goes away.

That framework is the device model. Learning it is what turns "I can write a module" into "I can write a driver".

Six concepts: kobjects and sysfs, bus / driver / device, probe and remove, devres, firmware description, and lifetime.


Concept 1: kobjects and sysfs

1. What problem it solves

The kernel has a large graph of related objects — devices, drivers, buses, classes — that need reference counting, a hierarchy, names, and a way for user space to see and configure them. Rather than solving that once per subsystem, the kernel embeds one small object, the kobject, in everything, and renders the resulting graph as a filesystem.

sysfs is not a reporting interface bolted onto the kernel. It is a view of the kernel's internal object graph, generated on read.

2. Where it exists in the kernel

ls include/linux/kobject.h include/linux/sysfs.h lib/kobject.c fs/sysfs/
$EDITOR Documentation/core-api/kobject.rst
$EDITOR Documentation/filesystems/sysfs.rst
ls /sys/ /sys/class/ /sys/bus/ /sys/devices/

3. Who owns or interacts with it

ActorInteraction
struct kobjectName, parent, refcount (kref), and a kobj_type describing its attributes
struct deviceEmbeds a kobject. Almost never manipulate the kobject directly.
sysfsRenders each kobject as a directory and each attribute as a file
udevConsumes the uevents kobjects emit on creation and removal
Every attribute fileBacked by a show() and optionally a store() function you write
   /sys/devices/…            THE OBJECTS — the real hierarchy, by topology
        ▲        ▲
        │        │  symlinks
   /sys/bus/…    /sys/class/…      VIEWS — by how it attaches / by what it does

   Each directory  = one kobject
   Each plain file = one struct attribute, with a show() behind it
   Each symlink    = a relationship between kobjects

4. Writing an attribute

The rules for sysfs are short and strictly enforced in review:

RuleWhy
One value per fileIt is an interface for cat, not a report format. A file with three fields is rejected.
No trailing prose, no units in the valueThe unit belongs in the documentation and the file name
Use sysfs_emit() in show()It knows the buffer is PAGE_SIZE and cannot overflow
Document it in Documentation/ABI/It is uapi. See the boundary chapter.
Attributes belong to the driver, via dev_groupsSo the core creates them before probe() returns and removes them after remove()
static ssize_t counter_show(struct device *dev,
                            struct device_attribute *attr, char *buf)
{
        struct mylab *ml = dev_get_drvdata(dev);

        /* sysfs_emit knows buf is PAGE_SIZE and handles the truncation. */
        return sysfs_emit(buf, "%u\n", READ_ONCE(ml->counter));
}

static ssize_t counter_store(struct device *dev, struct device_attribute *attr,
                             const char *buf, size_t count)
{
        struct mylab *ml = dev_get_drvdata(dev);
        unsigned int val;
        int ret;

        ret = kstrtouint(buf, 0, &val);       /* NOT simple_strtoul */
        if (ret)
                return ret;
        if (val > MYLAB_MAX)
                return -ERANGE;

        WRITE_ONCE(ml->counter, val);
        return count;                          /* return the bytes CONSUMED */
}
static DEVICE_ATTR_RW(counter);               /* creates dev_attr_counter    */

static struct attribute *mylab_attrs[] = {
        &dev_attr_counter.attr,
        NULL,
};
ATTRIBUTE_GROUPS(mylab);                       /* creates mylab_groups       */

static struct platform_driver mylab_driver = {
        .driver = {
                .name       = "mylab",
                .dev_groups = mylab_groups,    /* ← the core creates and
                                                *   destroys them for you    */
        },
        .probe = mylab_probe,
};

Warning: Creating attributes with device_create_file() inside probe() is the old way and it has a real race: user space (via udev) is notified the device exists before your attributes appear, so a rule that reads one can fail intermittently. dev_groups closes that window because the driver core creates the files before emitting the uevent. Converting an old driver from one to the other is a genuinely useful, well-understood patch.

5. Experiment

CLAIM. sysfs is a live view, not a snapshot, and the symlink structure encodes the difference between the object graph and its indexes.

METHOD.

# Pick a real device and walk it three ways.
IF=$(ls /sys/class/net | grep -v lo | head -1)

readlink -f /sys/class/net/$IF                 # → the real object in /sys/devices
ls -l /sys/class/net/$IF/device/driver 2>/dev/null   # → who claimed it
cat /sys/class/net/$IF/{address,mtu,operstate}

# Live, not cached:
cat /sys/class/net/$IF/statistics/rx_packets
ping -c 5 -q "$(ip route | awk '/^default/{print $3;exit}')" >/dev/null 2>&1
cat /sys/class/net/$IF/statistics/rx_packets

# And the object graph itself:
find /sys/devices -maxdepth 3 -name 'driver' -type l 2>/dev/null | head -5 | \
  while read -r d; do echo "$(dirname "$d") -> $(basename "$(readlink -f "$d")")"; done

PREDICT FIRST: is /sys/class/net/eth0 a directory or a symlink? Where does it point, and what does the answer tell you about which of /sys/class and /sys/devices holds the real object?

6. Failure mode

MistakeSymptom
Multiple values in one attribute fileRejected in review. Use one file per value, or debugfs for a dump.
sprintf into the show() bufferOverflow of a PAGE_SIZE buffer
store() returning something other than countUserspace write() loops forever, or reports a short write
Creating attributes in probe()A udev race that fails one time in a hundred
Putting a debugging counter in sysfsYou made a diagnostic permanent uapi. Use debugfs.
No Documentation/ABI/ entryReview comment; and nobody can find out what your file means

Concept 2: Bus, Driver, Device

1. What problem it solves

Hardware appears and disappears at times the kernel does not control: a PCI device found during enumeration, a USB stick plugged in, an I²C sensor described by firmware, a virtual device created by a hypervisor. Something must notice, decide which driver handles it, and call that driver.

The device model's answer is a three-way relationship with a matching function at the centre.

2. Where it exists in the kernel

ls drivers/base/
rg -n "struct bus_type \{" include/linux/device/bus.h
rg -n "struct device_driver \{" include/linux/device/driver.h
rg -n "struct device \{" include/linux/device.h
ls /sys/bus/

3. The relationship

   ┌──────────────────────────────────────────────────────────────────┐
   │  BUS   (platform, pci, usb, i2c, spi, virtio, amba, …)           │
   │        owns a list of DEVICES and a list of DRIVERS              │
   │        and ONE function:  match(device, driver) → bool           │
   └──────────────────────────────────────────────────────────────────┘
          │                                             │
          ▼                                             ▼
   ┌──────────────────┐                        ┌──────────────────────┐
   │ struct device    │   ◀── match() ──▶      │ struct device_driver │
   │ "this hardware   │                        │ "I can drive things  │
   │  exists"         │                        │  that look like X"   │
   └──────────────────┘                        └──────────────────────┘
          │                                             │
          └──────────────► BOUND ◄──────────────────────┘
                             │
                             ▼
                       driver->probe(device)

Both sides are registered dynamically and in either order. A driver that loads before its hardware appears simply waits; hardware that appears with no driver waits too, and binds the moment a matching driver is registered. Neither side polls.

4. Matching, per bus

BusMatches onTable
platformA firmware description (device tree compatible, ACPI _HID), or a nameof_match_table, acpi_match_table, id_table, or .driver.name
PCIVendor/device/class IDs read from config spacestruct pci_device_id[]
USBVendor/product IDs and interface class from descriptorsstruct usb_device_id[]
I²C / SPIFirmware description, or a board-file nameof_match_table + id_table
virtioDevice type IDstruct virtio_device_id[]
static const struct of_device_id mylab_of_match[] = {
        { .compatible = "mylab,widget-v1" },
        { .compatible = "mylab,widget-v2", .data = &v2_config },
        { }                                     /* terminator — REQUIRED */
};
MODULE_DEVICE_TABLE(of, mylab_of_match);
/*     ^ this is what makes AUTOLOADING work: it emits modalias strings into
 *       the module's metadata, which `depmod` indexes and `udev` matches
 *       against the uevent. Forget it and your module works only when
 *       insmod'ed by hand — a very common and very confusing bug.        */

static struct platform_driver mylab_driver = {
        .probe  = mylab_probe,
        .remove = mylab_remove,
        .driver = {
                .name           = "mylab",
                .of_match_table = mylab_of_match,
                .dev_groups     = mylab_groups,
        },
};
module_platform_driver(mylab_driver);     /* replaces init/exit boilerplate */

module_platform_driver() (and module_pci_driver(), module_i2c_driver(), …) expands to the module_init/module_exit pair that registers and unregisters the driver. Use it; hand-written boilerplate is a review comment.

5. Experiment

CLAIM. Binding is dynamic and reversible, and you can drive it by hand from sysfs.

METHOD.

# Find a bound device on any bus, then unbind and rebind it.
# Pick something harmless — NOT your root disk or your only NIC.
ls /sys/bus/platform/drivers/
DRV=/sys/bus/platform/drivers/$(ls /sys/bus/platform/drivers | head -1)
ls -l "$DRV"                     # symlinks to the devices it is bound to
DEV=$(ls "$DRV" | grep -v -E 'bind|unbind|uevent|module' | head -1)

echo "$DEV" | sudo tee "$DRV/unbind"     # calls remove()
ls -l "$DRV"                             # the symlink is gone
dmesg | tail -3
echo "$DEV" | sudo tee "$DRV/bind"       # calls probe() again
dmesg | tail -3

PREDICT FIRST: does unbind destroy the struct device, or only the binding? What appears in /sys/devices after the unbind?

RESULT. The device still exists; only the driver association was removed. That distinction — "the hardware is here" versus "something is driving it" — is the core of the model.

6. Failure mode

MistakeSymptom
Missing terminator in a match tableThe core walks off the end of the array
Missing MODULE_DEVICE_TABLEThe module never autoloads; it works only with manual insmod
Matching on .driver.name when a firmware match existsFragile; breaks on any platform that names things differently
Assuming a probe order between driversThere is none. Use -EPROBE_DEFER.
Assuming your device exists when your module loadsIt may not. Binding is asynchronous.

Concept 3: probe() and remove()

1. What problem it solves

probe() is where a driver meets its hardware: claim resources, map registers, register with subsystems, and expose interfaces. remove() is the exact inverse. Between them lies the entire lifetime of a bound driver, and almost every driver bug is an error in one of these two functions.

2. Where it exists in the kernel

rg -n "really_probe|driver_probe_device" drivers/base/dd.c | head
rg -n "deferred_probe" drivers/base/dd.c | head
cat /sys/kernel/debug/devices_deferred 2>/dev/null     # devices still waiting

3. What probe must do, in order

static int mylab_probe(struct platform_device *pdev)
{
        struct device *dev = &pdev->dev;
        struct mylab *ml;
        int ret;

        /* 1. Allocate driver state, tied to the device's lifetime. */
        ml = devm_kzalloc(dev, sizeof(*ml), GFP_KERNEL);
        if (!ml)
                return -ENOMEM;
        ml->dev = dev;
        mutex_init(&ml->lock);

        /* 2. Get resources described by firmware. Each may DEFER. */
        ml->regs = devm_platform_ioremap_resource(pdev, 0);
        if (IS_ERR(ml->regs))
                return dev_err_probe(dev, PTR_ERR(ml->regs), "cannot map registers\n");

        ml->clk = devm_clk_get_enabled(dev, NULL);
        if (IS_ERR(ml->clk))
                return dev_err_probe(dev, PTR_ERR(ml->clk), "cannot get clock\n");
        /*     ^ dev_err_probe() returns the error, logs it at the right level,
         *       and — crucially — logs -EPROBE_DEFER quietly instead of
         *       spamming the console once per retry.                        */

        /* 3. Read configuration. This API works for DT *and* ACPI. */
        ret = device_property_read_u32(dev, "mylab,depth", &ml->depth);
        if (ret)
                ml->depth = MYLAB_DEFAULT_DEPTH;      /* optional property */

        /* 4. Initialize deferred-work machinery BEFORE anything can fire. */
        INIT_WORK(&ml->work, mylab_work_fn);

        /* 5. Store the state so other callbacks can find it. */
        platform_set_drvdata(pdev, ml);

        /* 6. LAST: request the IRQ and register the user-visible interface.
         *    After this line, callbacks can run and user space can call in,
         *    so everything they touch must already be valid.               */
        ml->irq = platform_get_irq(pdev, 0);
        if (ml->irq < 0)
                return ml->irq;             /* it already logged the error   */

        ret = devm_request_threaded_irq(dev, ml->irq, NULL, mylab_isr_thread,
                                        IRQF_ONESHOT, "mylab", ml);
        if (ret)
                return dev_err_probe(dev, ret, "cannot request IRQ\n");

        dev_info(dev, "ready, depth %u\n", ml->depth);
        return 0;
}

The ordering rule in step 6 is the one to internalize: make the driver reachable last. Every resource an interrupt handler, a work item, or a file_operations callback might touch must be fully initialized before the thing that can invoke it is registered. A surprising number of real bugs are an IRQ registered three lines too early.

-EPROBE_DEFER, the mechanism that makes order-independence work:

   probe() needs a clock whose provider driver has not loaded yet.
        │
        └─▶ devm_clk_get() returns -EPROBE_DEFER
                 │
                 └─▶ probe() returns it unchanged
                          │
                          └─▶ the driver core puts this device on the
                              DEFERRED PROBE LIST and retries every time
                              any other driver successfully probes.

This is why you must return -EPROBE_DEFER unchanged rather than converting it to -ENODEV, and why dev_err_probe() exists: without it, a device that defers twenty times prints twenty identical scary errors.

cat /sys/kernel/debug/devices_deferred     # what is still waiting, and why
dmesg | grep -i "deferred probe"

4. Experiment

CLAIM. Probe order is genuinely undefined, and deferral is what makes drivers work anyway.

METHOD. In the guest, look at what actually deferred during boot:

dmesg | grep -iE "probe defer|deferred"
cat /sys/kernel/debug/devices_deferred 2>/dev/null
# The end-of-boot sweep, when the core gives up on stragglers:
dmesg | grep -i "deferred probe pending"

PREDICT FIRST: on a QEMU virt machine, will anything have deferred? Then add a deliberate deferral to your own driver:

static int mylab_probe(struct platform_device *pdev)
{
        static int tries;

        if (tries++ < 3) {
                dev_info(&pdev->dev, "pretending a dependency is missing\n");
                return -EPROBE_DEFER;
        }
        ...
}

Load it and watch how many times probe() is called, and what triggers each retry.

5. Failure mode

MistakeSymptom
Registering the IRQ before initializing what it touchesAn interrupt during probe dereferences a half-built structure
Registering a char device before its state is readySame, via user space
Converting -EPROBE_DEFER to another errorThe driver fails permanently instead of retrying
Using dev_err() for a deferralTwenty identical errors during boot
Assuming another driver has probedWorks on your board, fails on the next
remove() that does not exactly invert probe()Leaks, or use-after-free on unbind
Doing work in remove() after devm resources are goneThey are not — see the next section, carefully

Concept 4: devres

1. What problem it solves

The goto unwind chain from Kernel C is correct and tedious, and it must be duplicated in remove(). Most probe resources have exactly the lifetime of the driver binding. devres attaches them to the struct device so the core releases them automatically — on probe failure and on unbind — in reverse order.

A probe() written with devres has no goto chain and often no remove() at all.

2. Where it exists in the kernel

ls drivers/base/devres.c
$EDITOR Documentation/driver-api/driver-model/devres.rst   # the full list of devm_* APIs
rg -n "devm_kzalloc|devm_add_action_or_reset" include/linux/device.h

3. The API and the rule

Manualdevres
kzalloc / kfreedevm_kzalloc
ioremap / iounmapdevm_ioremap_resource, devm_platform_ioremap_resource
request_irq / free_irqdevm_request_irq, devm_request_threaded_irq
clk_get + clk_prepare_enable / …devm_clk_get, devm_clk_get_enabled
gpiod_get / gpiod_putdevm_gpiod_get
regulator_get / regulator_putdevm_regulator_get
Anything with no devm_ wrapperdevm_add_action_or_reset(dev, cleanup_fn, data)
/* For cleanup with no devm_ helper — this registers a callback that the
 * core will run at the right time, in the right order. The _or_reset
 * variant calls cleanup_fn immediately if REGISTERING the action fails,
 * which is what you almost always want.                                */
ret = devm_add_action_or_reset(dev, mylab_hw_shutdown, ml);
if (ret)
        return ret;

Warning: devres memory is freed after remove() returns. That is usually what you want and occasionally a trap. If your device, an interrupt, a work item, or a DMA engine can still touch a devm_kzalloced structure, you must stop that in remove() — the automatic free happens afterwards. devm_request_irq handles the IRQ case for you (it frees the IRQ at the right point in the reverse-order teardown), but a DMA engine you started, a timer you armed, or a work item you queued is yours to stop:

static void mylab_remove(struct platform_device *pdev)
{
        struct mylab *ml = platform_get_drvdata(pdev);

        WRITE_ONCE(ml->stopping, true);      /* no new work            */
        cancel_work_sync(&ml->work);         /* drain what is pending  */
        mylab_hw_quiesce(ml);                /* stop the hardware      */
        /* devm resources are released after this returns. */
}

The mixing trap. devres releases happen in reverse registration order, after remove(). A manually-freed resource in remove() therefore goes away before the devm ones. If a devm resource's release touches the thing you freed manually, you have a use-after-free. The rule: for any one device, do not mix — use devm for everything it can cover, and handle the rest with devm_add_action_or_reset so it joins the same ordered list.

4. Experiment

CLAIM. devres releases in reverse order, and you can observe it.

METHOD.

static void trace_release(void *data) { pr_info("release %s\n", (char *)data); }

static int lab_probe(struct platform_device *pdev)
{
        devm_add_action_or_reset(&pdev->dev, trace_release, "first");
        devm_add_action_or_reset(&pdev->dev, trace_release, "second");
        devm_add_action_or_reset(&pdev->dev, trace_release, "third");
        return 0;
}

PREDICT FIRST: in what order do the three messages appear on unbind? And now the more interesting one: make probe() return -EIO after registering all three. Do they still run? Which ones?

RESULT. Then check the accounting directly:

# devres has a debug interface when CONFIG_DEBUG_DEVRES is enabled:
grep CONFIG_DEBUG_DEVRES ~/kernel/build/.config
echo 1 | sudo tee /sys/module/drivers_base_devres/parameters/log 2>/dev/null
dmesg | grep devres | tail -20

5. Failure mode

MistakeSymptom
Mixing manual kfree and devm_kzalloc in one driverRelease ordering is wrong; use-after-free on unbind
Assuming devm memory is gone during remove()It is not; it is freed after. (The opposite assumption is the dangerous one.)
Leaving a DMA engine or timer running into remove()It writes into memory devres is about to free
devm_add_action instead of devm_add_action_or_resetA leak on the path where registering the action itself fails
devm_kzalloc for something outliving the bindingFreed while still referenced

Concept 5: Firmware Description — Device Tree and ACPI

1. What problem it solves

On a PC, PCI and USB enumerate themselves: the bus can be asked what is on it. On an embedded system, nothing enumerates. An I²C sensor at address 0x48 on bus 2, with its interrupt on GPIO 17 and its power from regulator vdd, is discoverable by no means whatsoever.

So something outside the kernel has to describe the hardware. Two ecosystems solved it differently, and the kernel supports both plus a unified API over them.

2. Where it exists in the kernel

ls drivers/of/ drivers/acpi/
ls Documentation/devicetree/bindings/ | head
ls arch/arm64/boot/dts/ | head
rg -n "device_property_read_u32|fwnode_property_read_u32" include/linux/property.h | head

3. The two, compared

Device TreeACPI
WhereEmbedded: ARM, RISC-V, PowerPCPCs, servers, some arm64
FormatA compiled binary blob (.dtb) from .dts sourceTables with a bytecode interpreter (AML)
Source of truthIn the kernel tree, under arch/*/boot/dts/In the platform firmware, not in the kernel
Matchingcompatible = "vendor,device"_HID / _CID
PropertiesNamed properties on nodes_DSD device properties
Bindings reviewA separate, strict review on devicetree@vger.kernel.orgPlatform-firmware vendors
/* arch/arm64/boot/dts/…/board.dts */
widget@40000000 {
        compatible = "mylab,widget-v1";
        reg        = <0x40000000 0x1000>;      /* → devm_platform_ioremap_resource */
        interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;  /* → platform_get_irq      */
        clocks     = <&clk_main>;              /* → devm_clk_get                   */
        mylab,depth = <8>;                     /* → device_property_read_u32       */
};

Use the unified property API, not the DT-specific one, unless you have a reason:

/* Works for device tree AND ACPI. Prefer these. */
device_property_read_u32(dev, "mylab,depth", &depth);
device_property_present(dev, "mylab,inverted");
device_property_read_string(dev, "label", &label);

/* The DT-only equivalents. Only when you genuinely need DT semantics. */
of_property_read_u32(dev->of_node, "mylab,depth", &depth);

Bindings are a separate contract. A new compatible string requires a YAML schema in Documentation/devicetree/bindings/, reviewed by the device-tree maintainers independently of your driver. It is uapi in all but name: a .dtb shipped in a device's firmware must keep working with future kernels.

ls Documentation/devicetree/bindings/ | head
make dt_binding_check DT_SCHEMA_FILES=Documentation/devicetree/bindings/…/mylab.yaml
make dtbs_check                      # validate the in-tree .dts files against the schemas
./scripts/get_maintainer.pl -f Documentation/devicetree/bindings/

Tip: Sending a driver and its binding in one series, with the binding patch first, is the expected shape. Reviewers of the binding are usually different people from the reviewers of the driver, and the binding gets the stricter reading — it is the part that cannot change later.

4. Experiment

CLAIM. You can add a device to a running QEMU guest's device tree and watch a driver bind to it.

METHOD. QEMU generates a device tree for -M virt; you can dump it, edit it, and boot with the result:

# Dump the DT QEMU generates:
qemu-system-aarch64 -M virt,dumpdtb=/tmp/virt.dtb -nographic -m 512 2>/dev/null
dtc -I dtb -O dts /tmp/virt.dtb -o /tmp/virt.dts
grep -n "compatible" /tmp/virt.dts | head -20

# Add a node for your driver, recompile, and boot with -dtb /tmp/mine.dtb.
# In the guest, the node appears under:
ls /proc/device-tree/
find /proc/device-tree -name compatible | head -10 | xargs -I{} sh -c 'echo -n "{}: "; tr -d "\0" < {}; echo'

PREDICT FIRST: if you add a node whose compatible matches your driver, does it bind at boot, at insmod, or not at all?

5. Failure mode

MistakeSymptom
A compatible string with no binding documentRejected by the DT maintainers
Changing an existing binding's meaningYou broke every shipped device tree that uses it
Using of_* APIs in a driver that must also work on ACPIIt works on one platform only
Reading a required property without checking the returnZero-initialized garbage when the property is absent
Hardcoding a register address or an IRQ numberThe driver works on exactly one board

Concept 6: Lifetime

1. What problem it solves

Three lifetimes overlap and are frequently confused: the module's, the struct device's, and the binding's. Getting them wrong is how a rmmod becomes a crash.

2. The three, and what holds each alive

LifetimeStartsEndsHeld by
Moduleinsmodrmmod, when the refcount is zerotry_module_get; fops.owner; a bound device
struct deviceThe bus creates it (enumeration, or firmware)put_device() drops the last reference → release()get_device()/put_device()
Bindingprobe() succeedsremove() — via unbind, module removal, or device removalThe driver core
   insmod mylab.ko
        └─▶ driver registered
                └─▶ core matches an existing device
                        └─▶ probe()             ← the binding begins
                                └─▶ user space opens /dev/mylab
                                        └─▶ fops.owner = THIS_MODULE holds
                                            a module reference, so
                                            `rmmod` now returns -EBUSY

Set .owner = THIS_MODULE in your file_operations (and equivalents). It is what makes rmmod refuse while a file descriptor is open. Without it, the module unloads, and the next read() on the still-open fd jumps into unmapped memory.

Every struct device needs a release() function. If you allocate one yourself, the core will tell you loudly when you did not:

Device 'mylab' does not have a release() function, it is broken and must be fixed.
See Documentation/core-api/kobject.rst.

And you may not free a struct device with kfree — its lifetime is the kobject's refcount, and someone else may still hold a reference. put_device(), and the release callback frees it.

3. Experiment

CLAIM. fops.owner is what stands between an open file descriptor and a kernel crash.

METHOD. With the char device from Lab 3:

# In the guest, with .owner = THIS_MODULE set:
exec 3</dev/mylab            # hold it open
lsmod | grep mylab           # note the "Used by" count
rmmod mylab                  # → rmmod: ERROR: Module mylab is in use
exec 3<&-                    # close it
rmmod mylab                  # → succeeds

PREDICT FIRST: now remove .owner = THIS_MODULE, rebuild, and repeat. Does rmmod succeed with the fd open? What happens on the next read() from fd 3? Predict before running — and run it in the guest, with KASAN on.

4. Failure mode

MistakeSymptom
No .owner = THIS_MODULErmmod succeeds while in use; the next syscall jumps into freed text
kfree on a struct deviceUse-after-free for whoever still held a reference
No release() functionA loud message from the driver core, and a leak or a crash
Unbalanced get_device/put_deviceEither a leak or a premature free
Assuming remove() means "the device is gone"It means the binding ended; the device may still exist and rebind

Putting It Together: A Driver's Skeleton

// SPDX-License-Identifier: GPL-2.0
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/property.h>

struct mylab { struct device *dev; void __iomem *regs; struct work_struct work;
               struct mutex lock; bool stopping; u32 depth; int irq; };

static int mylab_probe(struct platform_device *pdev) { /* as in Concept 3 */ }

static void mylab_remove(struct platform_device *pdev)
{
        struct mylab *ml = platform_get_drvdata(pdev);

        WRITE_ONCE(ml->stopping, true);
        cancel_work_sync(&ml->work);
        /* devm resources released after this returns, in reverse order. */
}

static const struct of_device_id mylab_of_match[] = {
        { .compatible = "mylab,widget-v1" },
        { }
};
MODULE_DEVICE_TABLE(of, mylab_of_match);

static struct platform_driver mylab_driver = {
        .probe  = mylab_probe,
        .remove = mylab_remove,
        .driver = {
                .name           = "mylab",
                .of_match_table = mylab_of_match,
                .dev_groups     = mylab_groups,
        },
};
module_platform_driver(mylab_driver);

MODULE_DESCRIPTION("A lab widget driver");
MODULE_AUTHOR("You <you@example.com>");
MODULE_LICENSE("GPL");

Note: The signature of .remove changed during the 6.x series — it returned int, and the return value was ignored and then removed because there is nothing the core can do if removal "fails". If your out-of-tree driver stops compiling here, that is the change; find it with git log -S'remove_new' or by reading struct platform_driver in your tree.


Validation / Self-check

  1. What is sysfs a view of? Which directory holds the real objects, and how did you prove it?
  2. Give the four rules for a sysfs attribute. Why is dev_groups preferred over creating files in probe()?
  3. Draw the bus/driver/device relationship and say where match() sits.
  4. In what order are a driver and its device registered? What guarantees the binding still happens?
  5. What does MODULE_DEVICE_TABLE do, and what is the exact symptom of forgetting it?
  6. List, in order, the six things probe() should do. Why is registering the IRQ last?
  7. What is -EPROBE_DEFER, who retries, and what must you not do with it?
  8. What does dev_err_probe() do that dev_err() plus return ret does not?
  9. When are devm resources released, relative to remove() returning? Give a bug that follows from getting that backwards.
  10. Why must you not mix manual kfree and devm_kzalloc in one driver?
  11. What is devm_add_action_or_reset for, and why the _or_reset variant?
  12. Compare device tree and ACPI on four axes. Which API works for both, and why should you prefer it?
  13. Why is a device-tree binding reviewed separately and more strictly than the driver?
  14. Name the three overlapping lifetimes and what holds each alive.
  15. What does .owner = THIS_MODULE prevent? Describe the crash that happens without it.
  16. Why may a struct device never be freed with kfree?

Next: Lab 1 — Build, Boot, and Attach a Debugger. The concepts are behind you; now build the machine you will test them on.