* [PATCH 1/8] watchdog: core: Clear wd_data pointer on errors
2026-09-29 13:46 [PATCH 0/8] watchdog: core: Fix locking, lifetime, suspend, and state management bugs Guenter Roeck
@ 2026-09-29 13:46 ` Guenter Roeck
2026-09-29 13:46 ` [PATCH 2/8] watchdog: core: Add missing locks Guenter Roeck
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Guenter Roeck @ 2026-09-29 13:46 UTC (permalink / raw)
To: linux-watchdog; +Cc: linux-kernel, Guenter Roeck
In watchdog_cdev_register(), if device registration fails after the core
watchdog_core_data structure is allocated and assigned to the persistent
watchdog_device structure, the function frees the data but leaves a
dangling pointer.
During device registration, watchdog_cdev_register() links the newly
allocated wd_data to wdd->wd_data. If a subsequent initialization step
fails, such as the watchdog_kworker validation, dev_set_name(),
misc_register(), or cdev_device_add(), the function cleans up by freeing
wd_data via kfree() or put_device(). However, it fails to clear the
wdd->wd_data pointer before returning. Furthermore, if cdev_device_add()
fails after misc_register() exposed /dev/watchdog to userspace, a
concurrent open may hold a reference to wd_data while the caller frees wdd,
leaving wd_data->wdd dangling.
Fix the problem by clearing wdd->wd_data on all error paths during
watchdog registration, and clearing wd_data->wdd under wd_data->lock if
cdev_device_add() fails.
Fixes: b4ffb1909843 ("watchdog: Separate and maintain variables based on variable lifetime")
Assisted-by: LLM
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
---
drivers/watchdog/watchdog_dev.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/drivers/watchdog/watchdog_dev.c b/drivers/watchdog/watchdog_dev.c
index e8c1fcb53c56..ed5b35184271 100644
--- a/drivers/watchdog/watchdog_dev.c
+++ b/drivers/watchdog/watchdog_dev.c
@@ -1033,6 +1033,7 @@ static int watchdog_cdev_register(struct watchdog_device *wdd)
wdd->wd_data = wd_data;
if (IS_ERR_OR_NULL(watchdog_kworker)) {
+ wdd->wd_data = NULL;
kfree(wd_data);
return -ENODEV;
}
@@ -1046,6 +1047,7 @@ static int watchdog_cdev_register(struct watchdog_device *wdd)
dev_set_drvdata(&wd_data->dev, wdd);
err = dev_set_name(&wd_data->dev, "watchdog%d", wdd->id);
if (err) {
+ wdd->wd_data = NULL;
put_device(&wd_data->dev);
return err;
}
@@ -1066,6 +1068,7 @@ static int watchdog_cdev_register(struct watchdog_device *wdd)
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;
}
@@ -1084,6 +1087,10 @@ static int watchdog_cdev_register(struct watchdog_device *wdd)
misc_deregister(&watchdog_miscdev);
old_wd_data = NULL;
}
+ 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;
}
--
2.45.2
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 2/8] watchdog: core: Add missing locks
2026-09-29 13:46 [PATCH 0/8] watchdog: core: Fix locking, lifetime, suspend, and state management bugs Guenter Roeck
2026-09-29 13:46 ` [PATCH 1/8] watchdog: core: Clear wd_data pointer on errors Guenter Roeck
@ 2026-09-29 13:46 ` Guenter Roeck
2026-09-29 13:46 ` [PATCH 3/8] watchdog: core: Prevent ping worker from re-arming timer on suspend Guenter Roeck
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Guenter Roeck @ 2026-09-29 13:46 UTC (permalink / raw)
To: linux-watchdog; +Cc: linux-kernel, Guenter Roeck
Add missing locks to watchdog_open(), watchdog_cdev_unregister(), and
watchdog_set_last_hw_keepalive(). Also protect old_wd_data with its own
lock to prevent its modification while in use, acquiring wd_data->lock
before releasing old_wd_data_lock in watchdog_open() to avoid a TOCTOU
race, and keeping misc_deregister() outside old_wd_data_lock to avoid
lock ordering inversion with misc_mtx.
Fixes: b4ffb1909843 ("watchdog: Separate and maintain variables based on variable lifetime")
Assisted-by: LLM
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
---
drivers/watchdog/watchdog_dev.c | 42 ++++++++++++++++++++++++++++-----
1 file changed, 36 insertions(+), 6 deletions(-)
diff --git a/drivers/watchdog/watchdog_dev.c b/drivers/watchdog/watchdog_dev.c
index ed5b35184271..50224877ff49 100644
--- a/drivers/watchdog/watchdog_dev.c
+++ b/drivers/watchdog/watchdog_dev.c
@@ -55,6 +55,7 @@
static dev_t watchdog_devt;
/* Reference to watchdog device behind /dev/watchdog */
static struct watchdog_core_data *old_wd_data;
+static DEFINE_MUTEX(old_wd_data_lock);
static struct kthread_worker *watchdog_kworker;
@@ -869,17 +870,32 @@ static int watchdog_open(struct inode *inode, struct file *file)
int err;
/* Get the corresponding watchdog device */
- if (imajor(inode) == MISC_MAJOR)
+ if (imajor(inode) == MISC_MAJOR) {
+ mutex_lock(&old_wd_data_lock);
wd_data = old_wd_data;
- else
+ if (!wd_data) {
+ mutex_unlock(&old_wd_data_lock);
+ return -ENODEV;
+ }
+ mutex_lock(&wd_data->lock);
+ mutex_unlock(&old_wd_data_lock);
+ } else {
wd_data = container_of(inode->i_cdev, struct watchdog_core_data,
cdev);
+ mutex_lock(&wd_data->lock);
+ }
/* the watchdog is single open! */
- if (test_and_set_bit(_WDOG_DEV_OPEN, &wd_data->status))
+ if (test_and_set_bit(_WDOG_DEV_OPEN, &wd_data->status)) {
+ mutex_unlock(&wd_data->lock);
return -EBUSY;
+ }
wdd = wd_data->wdd;
+ if (!wdd) {
+ err = -ENODEV;
+ goto out_clear;
+ }
/*
* If the /dev/watchdog device is open, we don't want the module
@@ -909,6 +925,8 @@ static int watchdog_open(struct inode *inode, struct file *file)
*/
wd_data->open_deadline = KTIME_MAX;
+ mutex_unlock(&wd_data->lock);
+
/* dev/watchdog is a virtual (and thus non-seekable) filesystem */
return stream_open(inode, file);
@@ -916,6 +934,7 @@ static int watchdog_open(struct inode *inode, struct file *file)
module_put(wd_data->wdd->ops->owner);
out_clear:
clear_bit(_WDOG_DEV_OPEN, &wd_data->status);
+ mutex_unlock(&wd_data->lock);
return err;
}
@@ -1085,7 +1104,9 @@ static int watchdog_cdev_register(struct watchdog_device *wdd)
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;
@@ -1131,9 +1152,12 @@ static void watchdog_cdev_unregister(struct watchdog_device *wdd)
cdev_device_del(&wd_data->cdev, &wd_data->dev);
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);
if (watchdog_active(wdd) &&
test_bit(WDOG_STOP_ON_UNREGISTER, &wdd->status)) {
watchdog_stop(wdd);
@@ -1141,7 +1165,6 @@ static void watchdog_cdev_unregister(struct watchdog_device *wdd)
watchdog_hrtimer_pretimeout_stop(wdd);
- mutex_lock(&wd_data->lock);
wd_data->wdd = NULL;
wdd->wd_data = NULL;
mutex_unlock(&wd_data->lock);
@@ -1208,20 +1231,27 @@ int watchdog_set_last_hw_keepalive(struct watchdog_device *wdd,
{
struct watchdog_core_data *wd_data;
ktime_t now;
+ int ret = 0;
if (!wdd)
return -EINVAL;
wd_data = wdd->wd_data;
+ if (!wd_data)
+ return -ENODEV;
+
+ mutex_lock(&wd_data->lock);
now = ktime_get();
wd_data->last_hw_keepalive = ktime_sub(now, ms_to_ktime(last_ping_ms));
if (watchdog_hw_running(wdd) && handle_boot_enabled)
- return __watchdog_ping(wdd);
+ ret = __watchdog_ping(wdd);
- return 0;
+ mutex_unlock(&wd_data->lock);
+
+ return ret;
}
EXPORT_SYMBOL_GPL(watchdog_set_last_hw_keepalive);
--
2.45.2
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 3/8] watchdog: core: Prevent ping worker from re-arming timer on suspend
2026-09-29 13:46 [PATCH 0/8] watchdog: core: Fix locking, lifetime, suspend, and state management bugs Guenter Roeck
2026-09-29 13:46 ` [PATCH 1/8] watchdog: core: Clear wd_data pointer on errors Guenter Roeck
2026-09-29 13:46 ` [PATCH 2/8] watchdog: core: Add missing locks Guenter Roeck
@ 2026-09-29 13:46 ` Guenter Roeck
2026-09-29 13:46 ` [PATCH 4/8] watchdog: core: Stop pretimeout hrtimer " Guenter Roeck
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Guenter Roeck @ 2026-09-29 13:46 UTC (permalink / raw)
To: linux-watchdog; +Cc: linux-kernel, Guenter Roeck
In watchdog_dev_suspend(), hrtimer_cancel() is called before
kthread_cancel_work_sync(). If the timer expired just before
hrtimer_cancel() and queued watchdog_ping_work(), the worker can run
and acquire wd_data->lock while kthread_cancel_work_sync() waits for it
to finish. Because no state flag indicates that the watchdog is
suspended, watchdog_worker_should_ping() returns true and
__watchdog_ping() re-arms wd_data->timer while the device is suspended.
Add a _WDOG_SUSPENDED internal status bit to wd_data->status, set it
under wd_data->lock in watchdog_dev_suspend(), clear it under
wd_data->lock in watchdog_dev_resume(), and check it in
watchdog_worker_should_ping(), watchdog_need_worker(), and
__watchdog_ping() so the timer cannot be armed while suspended.
Fixes: 60bcd91aafd2 ("watchdog: introduce watchdog_dev_suspend/resume")
Assisted-by: LLM
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
---
drivers/watchdog/watchdog_core.h | 1 +
drivers/watchdog/watchdog_dev.c | 15 +++++++++++----
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/drivers/watchdog/watchdog_core.h b/drivers/watchdog/watchdog_core.h
index ab825d9f9248..c6df5a584357 100644
--- a/drivers/watchdog/watchdog_core.h
+++ b/drivers/watchdog/watchdog_core.h
@@ -60,6 +60,7 @@ struct watchdog_core_data {
#define _WDOG_DEV_OPEN 0 /* Opened ? */
#define _WDOG_ALLOW_RELEASE 1 /* Did we receive the magic char ? */
#define _WDOG_KEEPALIVE 2 /* Did we receive a keepalive ? */
+#define _WDOG_SUSPENDED 3 /* Suspended ? */
};
/*
diff --git a/drivers/watchdog/watchdog_dev.c b/drivers/watchdog/watchdog_dev.c
index 50224877ff49..e38f1ef8e683 100644
--- a/drivers/watchdog/watchdog_dev.c
+++ b/drivers/watchdog/watchdog_dev.c
@@ -81,6 +81,9 @@ static inline bool watchdog_need_worker(struct watchdog_device *wdd)
unsigned int hm = wdd->max_hw_heartbeat_ms;
unsigned int t = wdd->timeout * 1000;
+ if (test_bit(_WDOG_SUSPENDED, &wdd->wd_data->status))
+ return false;
+
/*
* A worker to generate heartbeat requests is needed if all of the
* following conditions are true.
@@ -154,9 +157,10 @@ static int __watchdog_ping(struct watchdog_device *wdd)
now = ktime_get();
if (ktime_after(earliest_keepalive, now)) {
- hrtimer_start(&wd_data->timer,
- ktime_sub(earliest_keepalive, now),
- HRTIMER_MODE_REL_HARD);
+ if (!test_bit(_WDOG_SUSPENDED, &wd_data->status))
+ hrtimer_start(&wd_data->timer,
+ ktime_sub(earliest_keepalive, now),
+ HRTIMER_MODE_REL_HARD);
return 0;
}
@@ -207,7 +211,7 @@ static bool watchdog_worker_should_ping(struct watchdog_core_data *wd_data)
{
struct watchdog_device *wdd = wd_data->wdd;
- if (!wdd)
+ if (!wdd || test_bit(_WDOG_SUSPENDED, &wd_data->status))
return false;
if (watchdog_active(wdd))
@@ -1318,6 +1322,8 @@ int watchdog_dev_suspend(struct watchdog_device *wdd)
mutex_lock(&wd_data->lock);
if (watchdog_worker_should_ping(wd_data))
ret = __watchdog_ping(wd_data->wdd);
+ if (!ret)
+ set_bit(_WDOG_SUSPENDED, &wd_data->status);
mutex_unlock(&wd_data->lock);
if (ret)
@@ -1346,6 +1352,7 @@ int watchdog_dev_resume(struct watchdog_device *wdd)
* ping worker if needed.
*/
mutex_lock(&wd_data->lock);
+ clear_bit(_WDOG_SUSPENDED, &wd_data->status);
if (watchdog_worker_should_ping(wd_data))
ret = __watchdog_ping(wd_data->wdd);
mutex_unlock(&wd_data->lock);
--
2.45.2
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 4/8] watchdog: core: Stop pretimeout hrtimer on suspend
2026-09-29 13:46 [PATCH 0/8] watchdog: core: Fix locking, lifetime, suspend, and state management bugs Guenter Roeck
` (2 preceding siblings ...)
2026-09-29 13:46 ` [PATCH 3/8] watchdog: core: Prevent ping worker from re-arming timer on suspend Guenter Roeck
@ 2026-09-29 13:46 ` Guenter Roeck
2026-09-29 13:46 ` [PATCH 5/8] watchdog: core: Restore WDOG_HW_RUNNING if stopping watchdog fails Guenter Roeck
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Guenter Roeck @ 2026-09-29 13:46 UTC (permalink / raw)
To: linux-watchdog; +Cc: linux-kernel, Guenter Roeck
During system suspend, watchdog_dev_suspend() pings the watchdog (which
restarts the software pretimeout hrtimer) and stops the keepalive ping
worker, but fails to stop the pretimeout hrtimer. Because
CLOCK_MONOTONIC continues ticking until the system fully suspends, a
suspend process taking longer than the configured pretimeout will
trigger a spurious pretimeout event.
Stop the pretimeout hrtimer in watchdog_dev_suspend(), prevent
watchdog_hrtimer_pretimeout_start() from starting it while the watchdog
is suspended, and restart it in watchdog_dev_resume() if the hardware
watchdog is running.
Fixes: 60bcd91aafd2 ("watchdog: introduce watchdog_dev_suspend/resume")
Assisted-by: LLM
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
---
drivers/watchdog/watchdog_dev.c | 3 +++
drivers/watchdog/watchdog_hrtimer_pretimeout.c | 3 ++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/drivers/watchdog/watchdog_dev.c b/drivers/watchdog/watchdog_dev.c
index e38f1ef8e683..af756ee228b2 100644
--- a/drivers/watchdog/watchdog_dev.c
+++ b/drivers/watchdog/watchdog_dev.c
@@ -1334,6 +1334,7 @@ int watchdog_dev_suspend(struct watchdog_device *wdd)
* suspended
*/
hrtimer_cancel(&wd_data->timer);
+ watchdog_hrtimer_pretimeout_stop(wdd);
kthread_cancel_work_sync(&wd_data->work);
return 0;
@@ -1355,6 +1356,8 @@ int watchdog_dev_resume(struct watchdog_device *wdd)
clear_bit(_WDOG_SUSPENDED, &wd_data->status);
if (watchdog_worker_should_ping(wd_data))
ret = __watchdog_ping(wd_data->wdd);
+ if (watchdog_hw_running(wdd) && !ret)
+ watchdog_hrtimer_pretimeout_start(wdd);
mutex_unlock(&wd_data->lock);
return ret;
diff --git a/drivers/watchdog/watchdog_hrtimer_pretimeout.c b/drivers/watchdog/watchdog_hrtimer_pretimeout.c
index 49a05ea60c97..5c88e6467994 100644
--- a/drivers/watchdog/watchdog_hrtimer_pretimeout.c
+++ b/drivers/watchdog/watchdog_hrtimer_pretimeout.c
@@ -31,7 +31,8 @@ void watchdog_hrtimer_pretimeout_start(struct watchdog_device *wdd)
{
if (!(wdd->info->options & WDIOF_PRETIMEOUT) &&
wdd->pretimeout &&
- !watchdog_pretimeout_invalid(wdd, wdd->pretimeout))
+ !watchdog_pretimeout_invalid(wdd, wdd->pretimeout) &&
+ !test_bit(_WDOG_SUSPENDED, &wdd->wd_data->status))
hrtimer_start(&wdd->wd_data->pretimeout_timer,
ktime_set(wdd->timeout - wdd->pretimeout, 0),
HRTIMER_MODE_REL);
--
2.45.2
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 5/8] watchdog: core: Restore WDOG_HW_RUNNING if stopping watchdog fails
2026-09-29 13:46 [PATCH 0/8] watchdog: core: Fix locking, lifetime, suspend, and state management bugs Guenter Roeck
` (3 preceding siblings ...)
2026-09-29 13:46 ` [PATCH 4/8] watchdog: core: Stop pretimeout hrtimer " Guenter Roeck
@ 2026-09-29 13:46 ` Guenter Roeck
2026-09-29 13:46 ` [PATCH 6/8] watchdog: core: Cancel timer if cdev_device_add() fails Guenter Roeck
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Guenter Roeck @ 2026-09-29 13:46 UTC (permalink / raw)
To: linux-watchdog; +Cc: linux-kernel, Guenter Roeck
In watchdog_stop(), WDOG_HW_RUNNING is cleared before calling
wdd->ops->stop(). If wdd->ops->stop() returns an error, WDOG_ACTIVE
remains set, but WDOG_HW_RUNNING is left cleared. As a result,
subsequent calls to watchdog_ping() become no-ops, and
watchdog_release() sees watchdog_hw_running() as false and drops the
module and device references while the ping worker (guided by
WDOG_ACTIVE) continues to run.
Restore WDOG_HW_RUNNING if wdd->ops->stop() fails.
Fixes: 3c10bbde10fe ("watchdog: core: Clear WDOG_HW_RUNNING before calling the stop function")
Assisted-by: LLM
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
---
drivers/watchdog/watchdog_dev.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/drivers/watchdog/watchdog_dev.c b/drivers/watchdog/watchdog_dev.c
index af756ee228b2..edf2cccd1c0e 100644
--- a/drivers/watchdog/watchdog_dev.c
+++ b/drivers/watchdog/watchdog_dev.c
@@ -324,6 +324,8 @@ static int watchdog_stop(struct watchdog_device *wdd)
clear_bit(WDOG_ACTIVE, &wdd->status);
watchdog_update_worker(wdd);
watchdog_hrtimer_pretimeout_stop(wdd);
+ } else {
+ set_bit(WDOG_HW_RUNNING, &wdd->status);
}
return err;
--
2.45.2
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 6/8] watchdog: core: Cancel timer if cdev_device_add() fails
2026-09-29 13:46 [PATCH 0/8] watchdog: core: Fix locking, lifetime, suspend, and state management bugs Guenter Roeck
` (4 preceding siblings ...)
2026-09-29 13:46 ` [PATCH 5/8] watchdog: core: Restore WDOG_HW_RUNNING if stopping watchdog fails Guenter Roeck
@ 2026-09-29 13:46 ` Guenter Roeck
2026-09-29 13:46 ` [PATCH 7/8] watchdog: core: Fix unbalanced module_put() in watchdog_open() Guenter Roeck
2026-09-29 13:46 ` [PATCH 8/8] watchdog: core: Update last_keepalive in watchdog_start() Guenter Roeck
7 siblings, 0 replies; 9+ messages in thread
From: Guenter Roeck @ 2026-09-29 13:46 UTC (permalink / raw)
To: linux-watchdog; +Cc: linux-kernel, Guenter Roeck
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@roeck-us.net>
---
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
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 7/8] watchdog: core: Fix unbalanced module_put() in watchdog_open()
2026-09-29 13:46 [PATCH 0/8] watchdog: core: Fix locking, lifetime, suspend, and state management bugs Guenter Roeck
` (5 preceding siblings ...)
2026-09-29 13:46 ` [PATCH 6/8] watchdog: core: Cancel timer if cdev_device_add() fails Guenter Roeck
@ 2026-09-29 13:46 ` Guenter Roeck
2026-09-29 13:46 ` [PATCH 8/8] watchdog: core: Update last_keepalive in watchdog_start() Guenter Roeck
7 siblings, 0 replies; 9+ messages in thread
From: Guenter Roeck @ 2026-09-29 13:46 UTC (permalink / raw)
To: linux-watchdog; +Cc: linux-kernel, Guenter Roeck
If the hardware watchdog is running (hw_running is true), try_module_get()
is skipped. However, if watchdog_start() then fails, the code jumps to
out_mod and unconditionally calls module_put().
This can cause a premature driver module unload while hardware timers or
interrupts are active by dropping a reference that was never acquired.
Only call module_put() if the hardware watchdog is not running to fix the
problem.
Fixes: ee142889e32f ("watchdog: Introduce WDOG_HW_RUNNING flag")
Assisted-by: LLM
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
---
drivers/watchdog/watchdog_dev.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/watchdog/watchdog_dev.c b/drivers/watchdog/watchdog_dev.c
index 31567ffbfc23..24488c20539a 100644
--- a/drivers/watchdog/watchdog_dev.c
+++ b/drivers/watchdog/watchdog_dev.c
@@ -937,7 +937,8 @@ static int watchdog_open(struct inode *inode, struct file *file)
return stream_open(inode, file);
out_mod:
- module_put(wd_data->wdd->ops->owner);
+ if (!hw_running)
+ module_put(wd_data->wdd->ops->owner);
out_clear:
clear_bit(_WDOG_DEV_OPEN, &wd_data->status);
mutex_unlock(&wd_data->lock);
--
2.45.2
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 8/8] watchdog: core: Update last_keepalive in watchdog_start()
2026-09-29 13:46 [PATCH 0/8] watchdog: core: Fix locking, lifetime, suspend, and state management bugs Guenter Roeck
` (6 preceding siblings ...)
2026-09-29 13:46 ` [PATCH 7/8] watchdog: core: Fix unbalanced module_put() in watchdog_open() Guenter Roeck
@ 2026-09-29 13:46 ` Guenter Roeck
7 siblings, 0 replies; 9+ messages in thread
From: Guenter Roeck @ 2026-09-29 13:46 UTC (permalink / raw)
To: linux-watchdog; +Cc: linux-kernel, Guenter Roeck
When starting a watchdog whose hardware is already running,
watchdog_start() calls __watchdog_ping() before marking the watchdog
active, and fails to update wd_data->last_keepalive. As a result:
1. wd_data->last_keepalive remains 0, causing watchdog_get_timeleft() to
calculate the elapsed time since the epoch and return 0 time left.
2. watchdog_update_worker() inside __watchdog_ping() evaluates
watchdog_need_worker() and watchdog_next_keepalive() while WDOG_ACTIVE
is still clear, using wd_data->open_deadline instead of
wd_data->last_keepalive. If open_deadline has expired, the background
keepalive timer is not armed; conversely, if the active watchdog does
not need a worker, the boot keepalive timer is not canceled.
Set WDOG_ACTIVE and wd_data->last_keepalive to started_at before calling
__watchdog_ping(), and revert WDOG_ACTIVE and update the worker if
__watchdog_ping() fails.
Fixes: fbbe35dfcf94 ("watchdog: use __watchdog_ping in startup")
Assisted-by: LLM
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
---
drivers/watchdog/watchdog_dev.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/drivers/watchdog/watchdog_dev.c b/drivers/watchdog/watchdog_dev.c
index 24488c20539a..b9979f9957b8 100644
--- a/drivers/watchdog/watchdog_dev.c
+++ b/drivers/watchdog/watchdog_dev.c
@@ -264,10 +264,14 @@ static int watchdog_start(struct watchdog_device *wdd)
started_at = ktime_get();
if (watchdog_hw_running(wdd) && wdd->ops->ping) {
+ set_bit(WDOG_ACTIVE, &wdd->status);
+ wd_data->last_keepalive = started_at;
err = __watchdog_ping(wdd);
if (err == 0) {
- set_bit(WDOG_ACTIVE, &wdd->status);
watchdog_hrtimer_pretimeout_start(wdd);
+ } else {
+ clear_bit(WDOG_ACTIVE, &wdd->status);
+ watchdog_update_worker(wdd);
}
} else {
err = wdd->ops->start(wdd);
--
2.45.2
^ permalink raw reply [flat|nested] 9+ messages in thread