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
| Actor | Interaction |
|---|---|
struct kobject | Name, parent, refcount (kref), and a kobj_type describing its attributes |
struct device | Embeds a kobject. Almost never manipulate the kobject directly. |
sysfs | Renders each kobject as a directory and each attribute as a file |
udev | Consumes the uevents kobjects emit on creation and removal |
| Every attribute file | Backed 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:
| Rule | Why |
|---|---|
| One value per file | It is an interface for cat, not a report format. A file with three fields is rejected. |
| No trailing prose, no units in the value | The 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_groups | So 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()insideprobe()is the old way and it has a real race: user space (viaudev) is notified the device exists before your attributes appear, so a rule that reads one can fail intermittently.dev_groupscloses that window because the driver core creates the files before emitting theuevent. 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
| Mistake | Symptom |
|---|---|
| Multiple values in one attribute file | Rejected in review. Use one file per value, or debugfs for a dump. |
sprintf into the show() buffer | Overflow of a PAGE_SIZE buffer |
store() returning something other than count | Userspace 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 sysfs | You made a diagnostic permanent uapi. Use debugfs. |
No Documentation/ABI/ entry | Review 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
| Bus | Matches on | Table |
|---|---|---|
| platform | A firmware description (device tree compatible, ACPI _HID), or a name | of_match_table, acpi_match_table, id_table, or .driver.name |
| PCI | Vendor/device/class IDs read from config space | struct pci_device_id[] |
| USB | Vendor/product IDs and interface class from descriptors | struct usb_device_id[] |
| I²C / SPI | Firmware description, or a board-file name | of_match_table + id_table |
| virtio | Device type ID | struct 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
| Mistake | Symptom |
|---|---|
| Missing terminator in a match table | The core walks off the end of the array |
Missing MODULE_DEVICE_TABLE | The module never autoloads; it works only with manual insmod |
Matching on .driver.name when a firmware match exists | Fragile; breaks on any platform that names things differently |
| Assuming a probe order between drivers | There is none. Use -EPROBE_DEFER. |
| Assuming your device exists when your module loads | It 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
| Mistake | Symptom |
|---|---|
| Registering the IRQ before initializing what it touches | An interrupt during probe dereferences a half-built structure |
| Registering a char device before its state is ready | Same, via user space |
Converting -EPROBE_DEFER to another error | The driver fails permanently instead of retrying |
Using dev_err() for a deferral | Twenty identical errors during boot |
| Assuming another driver has probed | Works 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 gone | They 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
| Manual | devres |
|---|---|
kzalloc / kfree | devm_kzalloc |
ioremap / iounmap | devm_ioremap_resource, devm_platform_ioremap_resource |
request_irq / free_irq | devm_request_irq, devm_request_threaded_irq |
clk_get + clk_prepare_enable / … | devm_clk_get, devm_clk_get_enabled |
gpiod_get / gpiod_put | devm_gpiod_get |
regulator_get / regulator_put | devm_regulator_get |
Anything with no devm_ wrapper | devm_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:
devresmemory is freed afterremove()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 adevm_kzalloced structure, you must stop that inremove()— the automatic free happens afterwards.devm_request_irqhandles 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
| Mistake | Symptom |
|---|---|
Mixing manual kfree and devm_kzalloc in one driver | Release 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_reset | A leak on the path where registering the action itself fails |
devm_kzalloc for something outliving the binding | Freed 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 Tree | ACPI | |
|---|---|---|
| Where | Embedded: ARM, RISC-V, PowerPC | PCs, servers, some arm64 |
| Format | A compiled binary blob (.dtb) from .dts source | Tables with a bytecode interpreter (AML) |
| Source of truth | In the kernel tree, under arch/*/boot/dts/ | In the platform firmware, not in the kernel |
| Matching | compatible = "vendor,device" | _HID / _CID |
| Properties | Named properties on nodes | _DSD device properties |
| Bindings review | A separate, strict review on devicetree@vger.kernel.org | Platform-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
| Mistake | Symptom |
|---|---|
A compatible string with no binding document | Rejected by the DT maintainers |
| Changing an existing binding's meaning | You broke every shipped device tree that uses it |
Using of_* APIs in a driver that must also work on ACPI | It works on one platform only |
| Reading a required property without checking the return | Zero-initialized garbage when the property is absent |
| Hardcoding a register address or an IRQ number | The 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
| Lifetime | Starts | Ends | Held by |
|---|---|---|---|
| Module | insmod | rmmod, when the refcount is zero | try_module_get; fops.owner; a bound device |
struct device | The bus creates it (enumeration, or firmware) | put_device() drops the last reference → release() | get_device()/put_device() |
| Binding | probe() succeeds | remove() — via unbind, module removal, or device removal | The 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
| Mistake | Symptom |
|---|---|
No .owner = THIS_MODULE | rmmod succeeds while in use; the next syscall jumps into freed text |
kfree on a struct device | Use-after-free for whoever still held a reference |
No release() function | A loud message from the driver core, and a leak or a crash |
Unbalanced get_device/put_device | Either 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
.removechanged during the 6.x series — it returnedint, 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 withgit log -S'remove_new'or by readingstruct platform_driverin your tree.
Validation / Self-check
- What is
sysfsa view of? Which directory holds the real objects, and how did you prove it? - Give the four rules for a sysfs attribute. Why is
dev_groupspreferred over creating files inprobe()? - Draw the bus/driver/device relationship and say where
match()sits. - In what order are a driver and its device registered? What guarantees the binding still happens?
- What does
MODULE_DEVICE_TABLEdo, and what is the exact symptom of forgetting it? - List, in order, the six things
probe()should do. Why is registering the IRQ last? - What is
-EPROBE_DEFER, who retries, and what must you not do with it? - What does
dev_err_probe()do thatdev_err()plusreturn retdoes not? - When are
devmresources released, relative toremove()returning? Give a bug that follows from getting that backwards. - Why must you not mix manual
kfreeanddevm_kzallocin one driver? - What is
devm_add_action_or_resetfor, and why the_or_resetvariant? - Compare device tree and ACPI on four axes. Which API works for both, and why should you prefer it?
- Why is a device-tree binding reviewed separately and more strictly than the driver?
- Name the three overlapping lifetimes and what holds each alive.
- What does
.owner = THIS_MODULEprevent? Describe the crash that happens without it. - Why may a
struct devicenever be freed withkfree?
Next: Lab 1 — Build, Boot, and Attach a Debugger. The concepts are behind you; now build the machine you will test them on.