Root Cause Analysis
1. Involved Components
- Source File:
kernel/time/posix-clock.c
- Related Files:
fs/char_dev.c, fs/file_table.c, drivers/ptp/ptp_clock.c
- Structs and Fields:
struct ptp_clock: embeds struct posix_clock clock (at offset 0) and struct device dev (at offset 0x158).
struct posix_clock: embeds struct cdev cdev (at offset 80 / +0x50) and holds pointer struct device *dev (at offset 184 / +0xb8).
struct cdev: field struct module *owner (at offset 64 / +0x40 inside struct cdev, making its offset within struct ptp_clock equal to 144 / 0x90).
2. Mechanism of the Defect
In kernel/time/posix-clock.c, posix_clock_register() initializes and registers a character device for POSIX clock devices (such as PTP clocks):
int posix_clock_register(struct posix_clock *clk, struct device *dev)
{
int err;
init_rwsem(&clk->rwsem);
cdev_init(&clk->cdev, &posix_clock_file_operations);
err = cdev_device_add(&clk->cdev, dev);
if (err) {
pr_err("%s unable to add device %d:%d\n",
dev_name(dev), MAJOR(dev->devt), MINOR(dev->devt));
return err;
}
clk->cdev.owner = clk->ops.owner;
clk->dev = dev;
return 0;
}
Notice the order of operations:
cdev_device_add(&clk->cdev, dev) internally calls cdev_set_parent(cdev, &dev->kobj), cdev_add(cdev, dev->devt, 1), and device_add(dev).
- As soon as
cdev_add() finishes, the character device is registered in cdev_map and immediately exposed to userspace.
- Crucially,
clk->dev = dev; and clk->cdev.owner = clk->ops.owner; are only assigned after cdev_device_add() successfully completes.
Because struct ptp_clock was allocated with kzalloc_obj() in ptp_clock_register(), clk->dev is initialized to NULL.
3. Invariant Violation and Failure Flow
-
Window of Race:
During driver probe or PCI rescan (e.g. e1000_probe -> e1000e_ptp_init -> ptp_clock_register), while task 1 is executing cdev_device_add() inside posix_clock_register(), userspace task 2 opens /dev/ptp0.
-
Failed Reference Increment in posix_clock_open():
When task 2 opens the device node, posix_clock_open() runs:
fp->private_data = pccontext;
get_device(clk->dev);
Because clk->dev is still NULL, get_device(NULL) is executed, which is a no-op that returns NULL without incrementing the reference count on &ptp->dev.
-
Subsequent Initialization:
Task 1 completes cdev_device_add() and assigns clk->dev = dev;.
-
Unbalanced Reference Drop in posix_clock_release():
When task 2 closes /dev/ptp0 (during process exit via close_files() -> filp_close_sync() -> __fput()), __fput() executes:
if (file->f_op->release)
file->f_op->release(inode, file);
if (unlikely(S_ISCHR(inode->i_mode) && inode->i_cdev != NULL &&
!(mode & FMODE_PATH))) {
cdev_put(inode->i_cdev);
}
First, file->f_op->release() calls posix_clock_release(), which runs:
put_device(clk->dev);
Now clk->dev is non-NULL (&ptp->dev). Because get_device() was never invoked during open, this put_device() drops a reference that was never acquired, leading to an extra/unbalanced decrement.
-
Premature Deallocation & Use-After-Free:
The unbalanced put_device(&ptp->dev) decrements ptp->dev.kobj's reference count to zero. This triggers device_release() -> ptp_clock_release():
static void ptp_clock_release(struct device *dev)
{
...
kfree(ptp);
}
The entire memory block holding struct ptp_clock (including ptp->clock.cdev) is freed.
Immediately after file->f_op->release() returns in __fput(), line 515 executes:
cdev_put(inode->i_cdev);
In cdev_put(struct cdev *p) (fs/char_dev.c):
void cdev_put(struct cdev *p)
{
if (p) {
struct module *owner = p->owner;
kobject_put(&p->kobj);
module_put(owner);
}
}
Here p is &ptp->clock.cdev. Dereferencing p->owner reads offset 144 (0x90) inside the freed 4096-byte kmalloc-4k slab object, directly triggering the reported slab-use-after-free read.