[PATCH 6/8] watchdog: core: Cancel timer if cdev_device_add() fails

From: Guenter Roeck

Date: Tue Sep 29 2026 - 09:56:55 EST


If misc_register() exposed the device to userspace before cdev_device_add()
is called, a concurrent watchdog_open() could start the watchdog and arm
wd_data->timer as well as the pretimeout timer. Also, if the hardware
watchdog was already running, watchdog_open() expects the device and module
references to have been acquired prior to opening.

If cdev_device_add() then fails, the error path drops the device reference
but fails to stop the watchdog, cancel the timers, and stop the worker,
leaving the watchdog active and timers armed that can later fire and
dereference freed memory. Furthermore, if a concurrent watchdog_open() saw
hw_running == true before cdev_device_add() was called, it skipped taking
its own device reference, allowing put_device() on the error path to free
wd_data while the file descriptor is still open. Similarly, when
unregistering a running watchdog that is not currently open, the extra
hw_running module and device references were never released.

Fix the problem by initializing wd_data and taking the running-watchdog
references before exposing the device via misc_register(), stopping the
watchdog and canceling both the heartbeat and pretimeout timers and
stopping the worker on registration failure, and releasing unclaimed
running-watchdog references on registration failure and unregistration.

Fixes: ee142889e32f ("watchdog: Introduce WDOG_HW_RUNNING flag")
Assisted-by: LLM
Signed-off-by: Guenter Roeck <linux@xxxxxxxxxxxx>
---
drivers/watchdog/watchdog_dev.c | 91 ++++++++++++++++++++-------------
1 file changed, 55 insertions(+), 36 deletions(-)

diff --git a/drivers/watchdog/watchdog_dev.c b/drivers/watchdog/watchdog_dev.c
index edf2cccd1c0e..31567ffbfc23 100644
--- a/drivers/watchdog/watchdog_dev.c
+++ b/drivers/watchdog/watchdog_dev.c
@@ -1047,6 +1047,7 @@ static const struct class watchdog_class = {
static int watchdog_cdev_register(struct watchdog_device *wdd)
{
struct watchdog_core_data *wd_data;
+ bool hw_running;
int err;

wd_data = kzalloc_obj(struct watchdog_core_data);
@@ -1082,46 +1083,10 @@ static int watchdog_cdev_register(struct watchdog_device *wdd)
HRTIMER_MODE_REL_HARD);
watchdog_hrtimer_pretimeout_init(wdd);

- if (wdd->id == 0) {
- old_wd_data = wd_data;
- watchdog_miscdev.parent = wdd->parent;
- err = misc_register(&watchdog_miscdev);
- if (err != 0) {
- pr_err("%s: cannot register miscdev on minor=%d (err=%d).\n",
- wdd->info->identity, WATCHDOG_MINOR, err);
- if (err == -EBUSY)
- pr_err("%s: a legacy watchdog module is probably present.\n",
- wdd->info->identity);
- old_wd_data = NULL;
- wdd->wd_data = NULL;
- put_device(&wd_data->dev);
- return err;
- }
- }
-
/* Fill in the data structures */
cdev_init(&wd_data->cdev, &watchdog_fops);
wd_data->cdev.owner = wdd->ops->owner;

- /* Add the device */
- err = cdev_device_add(&wd_data->cdev, &wd_data->dev);
- if (err) {
- pr_err("watchdog%d unable to add device %d:%d\n",
- wdd->id, MAJOR(watchdog_devt), wdd->id);
- if (wdd->id == 0) {
- misc_deregister(&watchdog_miscdev);
- mutex_lock(&old_wd_data_lock);
- old_wd_data = NULL;
- mutex_unlock(&old_wd_data_lock);
- }
- mutex_lock(&wd_data->lock);
- wd_data->wdd = NULL;
- wdd->wd_data = NULL;
- mutex_unlock(&wd_data->lock);
- put_device(&wd_data->dev);
- return err;
- }
-
/* Record time of most recent heartbeat as 'just before now'. */
wd_data->last_hw_keepalive = ktime_sub(ktime_get(), 1);
watchdog_set_open_deadline(wd_data);
@@ -1141,7 +1106,55 @@ static int watchdog_cdev_register(struct watchdog_device *wdd)
wdd->id);
}

+ if (wdd->id == 0) {
+ old_wd_data = wd_data;
+ watchdog_miscdev.parent = wdd->parent;
+ err = misc_register(&watchdog_miscdev);
+ if (err != 0) {
+ pr_err("%s: cannot register miscdev on minor=%d (err=%d).\n",
+ wdd->info->identity, WATCHDOG_MINOR, err);
+ if (err == -EBUSY)
+ pr_err("%s: a legacy watchdog module is probably present.\n",
+ wdd->info->identity);
+ old_wd_data = NULL;
+ goto err_clear;
+ }
+ }
+
+ /* Add the device */
+ err = cdev_device_add(&wd_data->cdev, &wd_data->dev);
+ if (err) {
+ pr_err("watchdog%d unable to add device %d:%d\n",
+ wdd->id, MAJOR(watchdog_devt), wdd->id);
+ if (wdd->id == 0) {
+ misc_deregister(&watchdog_miscdev);
+ mutex_lock(&old_wd_data_lock);
+ old_wd_data = NULL;
+ mutex_unlock(&old_wd_data_lock);
+ }
+ goto err_clear;
+ }
+
return 0;
+
+err_clear:
+ mutex_lock(&wd_data->lock);
+ hw_running = watchdog_hw_running(wdd);
+ if (watchdog_active(wdd))
+ watchdog_stop(wdd);
+ watchdog_hrtimer_pretimeout_stop(wdd);
+ if (hw_running && !test_bit(_WDOG_DEV_OPEN, &wd_data->status)) {
+ module_put(wdd->ops->owner);
+ put_device(&wd_data->dev);
+ }
+ wd_data->wdd = NULL;
+ wdd->wd_data = NULL;
+ mutex_unlock(&wd_data->lock);
+
+ hrtimer_cancel(&wd_data->timer);
+ kthread_cancel_work_sync(&wd_data->work);
+ put_device(&wd_data->dev);
+ return err;
}

/**
@@ -1154,6 +1167,7 @@ static int watchdog_cdev_register(struct watchdog_device *wdd)
static void watchdog_cdev_unregister(struct watchdog_device *wdd)
{
struct watchdog_core_data *wd_data = wdd->wd_data;
+ bool hw_running;

cdev_device_del(&wd_data->cdev, &wd_data->dev);
if (wdd->id == 0) {
@@ -1164,6 +1178,7 @@ static void watchdog_cdev_unregister(struct watchdog_device *wdd)
}

mutex_lock(&wd_data->lock);
+ hw_running = watchdog_hw_running(wdd);
if (watchdog_active(wdd) &&
test_bit(WDOG_STOP_ON_UNREGISTER, &wdd->status)) {
watchdog_stop(wdd);
@@ -1171,6 +1186,10 @@ static void watchdog_cdev_unregister(struct watchdog_device *wdd)

watchdog_hrtimer_pretimeout_stop(wdd);

+ if (hw_running && !test_bit(_WDOG_DEV_OPEN, &wd_data->status)) {
+ module_put(wdd->ops->owner);
+ put_device(&wd_data->dev);
+ }
wd_data->wdd = NULL;
wdd->wd_data = NULL;
mutex_unlock(&wd_data->lock);
--
2.45.2