Skip to content

wdt: make the watchdog reset the board when the thing it guards dies - #223

Merged
widgetii merged 7 commits into
mainfrom
wdt-linux-api
Sep 12, 2026
Merged

widgetii merged 7 commits into
mainfrom
wdt-linux-api

Conversation

@widgetii

Copy link
Copy Markdown
Member

Fixes OpenIPC/firmware#1803.

The watchdog driver did not honour the Linux watchdog API in six ways, and the
driver exists ten times in this tree, so each one had to be found more than
once. This fixes all six and retires the duplication where the copies were the
same driver.

The defects

Measured on lab boards, kernel 4.9.37 and 3.18.20.

1. The margin meant twice itself. set_timeout() loaded a whole margin into
a counter the SP805 runs twice — reaching zero raises the interrupt and reloads,
and only a second zero with that interrupt still pending drives the reset. A
margin of 30 s reset the board at 58 s. Loading half a margin is what
drivers/watchdog/sp805_wdt.c does.

2. A ping did nothing for half the cycle. feed() read WDT_RIS first and
returned without touching anything when no interrupt was pending, so the
deadline stayed where the previous accepted ping had put it. The honest cadence
of one ping per half-margin was therefore worth exactly one accepted ping per
cycle: miss it and a process that had been feeding the device all along got the
board reset under it. The cv500 and V5 copies were worse — even when the
interrupt was pending they only cleared it, never reloading.

3. An unexpected close handed the dog back to the driver. This is #1803.
The driver runs a kernel thread that feeds the watchdog whenever dog_state is
SELFCLR; open() set EXTCLR so the thread stood down, but release() set
SELFCLR again — unconditionally. So the one event the device exists for, the
guarded process dying and the kernel closing its descriptor, handed the watchdog
straight back to the driver, which then fed it for the rest of the board's life.
Nothing ever reset. The only workaround available was nodeamon=1: no feeder at
all, and therefore no cover for the module-load-to-first-open window either.

WDIOF_MAGICCLOSE has been advertised throughout, and it means a close without
a preceding "V" leaves the timer running. The feeder now covers the pre-open
window and nothing else — open() latches EXTCLR one-way and release() no
longer undoes it — and a write handler makes the magic close reachable at all.

board before after
hi3516ev300 majestic SIGKILLed at uptime 40195, still up at 40400 reset at 27–36 s
hi3516av300 SIGKILLed at 215043, still up at 215249 reset at 27–36 s
gk7205v200 probe died at uptime 28050, still up 164 s later reset at 27–36 s

4. open() did not start the timer. A magic close, or WDIOC_SETOPTIONS
with WDIOS_DISABLECARD, clears WDT_CTRL, and neither the ping in open()
nor WDIOC_SETTIMEOUT puts it back. From the first deliberate stop the device
was inert while looking entirely healthy: the open succeeded, the margin was
accepted and read back, the pings were accepted, and nothing would ever have
reset the board. majestic did not notice because it sends WDIOS_ENABLECARD
itself right after opening; a client that only does what the API documents
(busybox watchdog, systemd) got nothing.

5. rmmod could wedge the board permanently. watchdog_exit() stopped the
feeder before unregistering the device. dog_exit() sets DOG_EXIT and then
blocks in kthread_stop() waiting for the thread to notice — and the thread's
loop condition is dog_state, which open() sets straight back to EXTCLR.
One open racing the unload left the thread looping for ever:

rmmod   D  kthread_stop
           osal_kthread_destory [open_osal]
           watchdog_exit [open_wdt]
           wdg_remove [open_wdt]
           platform_drv_remove
           SyS_delete_module

module_mutex held, the task unkillable, and the board did not survive a clean
reboot — it needed the power pulled. Hit on a lab gk7205v200 by nothing more
exotic than running a probe against /dev/watchdog while an rmmod was in
flight, and unrecoverable once lost: release() leaves the state at SELFCLR
and open() at EXTCLR, neither of which is DOG_EXIT. Unregistering first
closes the window. Verified on the same board — with a shell loop opening and
closing /dev/watchdog as fast as it can, rmmod now returns 0 in two seconds.

6. hi3516cv300 had no watchdog at all. wdt/hi3516cv300/hi_wdt.c contained
no module entry point and its kbuild linked no init object, so
hi3516cv300_wdt.ko loaded, did nothing, and the board came up with no
/dev/watchdog
. majestic logged Watchdog device (/dev/watchdog) not found
and every cv300 ran unguarded. Confirmed on a lab hi3516cv300 before this
change.

Smaller things alongside: WDIOC_GETTIMELEFT (was EPERM everywhere);
-EINVAL for an out-of-range margin instead of clamping to one nobody asked
for; SETTIMEOUT reading back the margin in force; -ENOTTY for an unknown
ioctl; and a feeder paced on the margin in force rather than the module default,
so a caller that lowers the margin and hands the device back is not reset on a
cycle nobody configured.

The consolidation

hi3516cv500 and hi3516cv300 now build wdt/wdt.c and wdt/wdt_init.c. What
actually differs comes from the kbuild:

WDT_BASE the register map
WDT_OSAL_HI OSAL is hi_osal.h here, not osal.h
WDT_OSAL_NO_KTHREAD_STOP_FLAG V3's osal_kthread_destory() takes the task alone and always stops it
WDT_NO_OF no watchdog node in the device tree — register on load and map WDT_BASE directly
WDT_SCTL_BASE the 3 MHz clock is gated in the system controller and needs ungating first

wdt_init.c carries both binding styles and names the device-tree compatibles
outright rather than assembling one from compat.h, which only knows the two V4
chips; that retires init/hi3516cv500/wtdg_init.c too. DOG_TIMER_MARGIN moved
into wdt/watchdog.h, where wdt_init.c can see it — the parameter description
used to stringify the undefined token and print (default=DOG_TIMER_MARGIN).

Net −837 lines.

Verified

Four boards, both binding styles. Crash path is open, SETTIMEOUT(30), die
without "V"; the board must reset one margin later and come back.

board binding base crash → reset GETTIMELEFT magic close clean stop
hi3516cv300 WDT_NO_OF 0x12080000 30 s (was: no device) 299/300 1 no reset
hi3516av300 platform 0x12051000 30 s (was: never) 299/300 1 no reset
hi3516ev300 platform 0x12030000 30 s (was: never) 299/300 1 no reset
gk7205v200 platform 0x12030000 30 s (was: never) 299/300 1 no reset

Soaks with majestic feeding at its own cadence, past the 300 s margin: cv300
456 s, av300 491 s, ev300 1327 s, no reboot. On cv300 majestic now logs
Detected Watchdog / Watchdog timeout set to 300 seconds for the first time.

Builds clean for hi3516cv300, hi3516cv500, hi3516ev200 (both output-ev200 and
output-hi3516ev300) and gk7205v200 (both output-gk and output-gk3).

Not consolidated, and why

  • hi3516cv6xx, hi3519dv500 — a later vendor generation: an optional FIQ
    feeding path, an hrtimer, ot_osal.h, and a command-list ioctl dispatch
    rather than unlocked_ioctl. All six fixes are carried over to both in place
    instead. There is no firmware tree on this machine for either, so those two
    files are reviewed but neither compiled nor run
    — the one untested part of
    this branch, and the thing most worth a second pair of eyes.
  • hi3516cv100, hi3519v101 — their kbuild links a vendor .o; the source
    beside it is reference and reaches no board. Benefiting needs a blob→source
    switch first.
  • hi3516cv200, hi3516av100, hi3520dv200 — V2 has no OSAL at all (the
    osal_v2_shim is a kernel-API compat shim for blobs, not an OSAL), and the
    driver there is written against plain kernel APIs. Sharing wdt.c would mean
    writing an OSAL for the generation first. WDT_SCTL_BASE exists in wdt.c
    ready for them.

Also related: OpenIPC/firmware#127 asks for the watchdog module on
hi3516cv200/hi3518ev200. It is already insmoded there (the #nodeamon=1
beside it in load_hisilicon is a comment, not a parameter) — but that .ko is
a prebuilt vendor blob in the firmware package, so none of this reaches those
boards until it is built from source.

Worth a release note

cv300 goes from having no watchdog to having a working one. That is the fix, but
it does mean a cv300 whose majestic wedges will now reboot where it previously
sat there. Benign, and the point of the device, but it is a behaviour change in
the field.

A camera on the field reported rebooting in a loop, healthy the whole
time, and cured only by telling its streamer not to enable the watchdog
at all. That is what this device does to anyone who takes the Linux
watchdog API at its word.

## The margin meant half of itself

The SP805 runs its load value twice: reaching zero raises the interrupt
and reloads, and only reaching zero a *second* time with that interrupt
still pending drives the reset. `dog_set_timeout()` loaded a whole
margin, so every timeout was worth twice what it said — the vendor
readme even documented it, "a reset signal will be sended to the system
after 2 timer interrupt". Measured on a lab gk7205v200, `WDIOC_SETTIMEOUT(30)`
then no feeding: the board came back at 58 s.

Load half a margin, as `drivers/watchdog/sp805_wdt.c` does. Same board,
same test: 29 s.

## A ping did nothing for the first half of every cycle

`dog_feed()` read `WDT_RIS` first and returned without touching a
register when no interrupt was pending. So a ping only counted once the
counter had already run out — the deadline stayed wherever the previous
*accepted* ping had put it, and the textbook cadence of one ping per
half-margin was worth exactly one accepted ping per cycle. Miss it and a
process that had been feeding the device all along gets the board reset
under it.

Write the load and clear the interrupt unconditionally, which is what
WDIOC_KEEPALIVE means and what sp805_wdt.c does. Proven through the new
GETTIMELEFT on the lab board: margin 60, time left falls 59 -> 39 over
twenty seconds, and a single KEEPALIVE puts it back to 59. Before, that
ping moved nothing.

## And it could not be stopped

`ident.options` has advertised `WDIOF_MAGICCLOSE` all along with
`dog_write()` compiled out, so the documented way to say "this shutdown
is deliberate" — write "V", then close — returned EINVAL, and a process
that started the device had no way to stop it. `dog_write()` is enabled
and wired into `dog_fops`; `dog_release()` honours it and otherwise says
"Unexpected close, not stopping watchdog!" like every other driver, plus
a line naming `nodeamon=1` as the case where nothing is left feeding.

## The rest of the API surface

- `WDIOC_SETTIMEOUT` reports back the margin programmed, falling through
  to GETTIMEOUT the way the watchdog core does, and leaves the margin
  alone on a value out of range instead of half-applying it.
- `WDIOC_GETTIMELEFT` is implemented, and uncommented in `watchdog.h`.
  It adds the outstanding load back when no interrupt is pending, so it
  answers seconds-to-reset rather than seconds-to-first-expiry.
- Errors are `-EINVAL` / `-ENOTTY` rather than a bare `-1`, which
  userspace was reading as EPERM.
- `dog_deamon` paces itself off the margin in force rather than off
  `default_margin`. Handed a device whose margin userspace had made
  smaller, it was feeding slower than the margin it was feeding, and
  reset the board on a cycle nobody configured. Capped at 30 s so a long
  margin cannot make module unload wait out a sleep.
- `expect_close` is cleared on open, so a "V" from a previous opener
  cannot stop the timer for the next one.

`readme-eng.txt` and the `default_margin` parameter description say the
new contract; the old bound in the latter ("0<default_margin<80") was
never the real one.

Built for gk7205v200 against the OpenIPC 4.9.37 kernel and run on a lab
gk7205v200: the numbers above, then majestic on top of it feeding at its
own cadence, uptime past 900 s.
The driver's own kernel thread fed the watchdog whenever dog_state was
DOG_SELFCLR, and dog_release() set DOG_SELFCLR on every close. So the
one event the device exists for -- the process being guarded dies, and
the kernel closes its descriptor for it -- handed the watchdog straight
back to the driver, which then fed it for the rest of the board's life.
Nothing ever reset. OpenIPC/firmware#1803 reports exactly this on
hi3516ev300 and gk7205v300, and the only workaround available was
nodeamon=1: no kernel feeder at all, and therefore no cover for the
window between module load and the first open either.

WDIOF_MAGICCLOSE has been advertised all along, and it means a close
without a preceding "V" leaves the timer running. Honour that: stop
the device only when the caller asked for it, and otherwise leave it
armed and unfed. The feeder now covers the pre-open window and nothing
else -- dog_open() latches DOG_EXTCLR and dog_release() no longer
undoes it.

Measured on a lab hi3516ev300, stock module: majestic SIGKILLed at
uptime 40195, board still up and counting at 40400.
watchdog-api.txt is unambiguous -- the timer runs from the moment
/dev/watchdog is opened -- and that held here only for as long as
nothing had deliberately stopped the counter. A magic close, or
WDIOC_SETOPTIONS with WDIOS_DISABLECARD, clears WDT_CTRL, and neither
the ping in dog_open() nor WDIOC_SETTIMEOUT puts it back. From the
first deliberate stop until someone thought to send WDIOS_ENABLECARD
the device was inert while looking entirely healthy: the open
succeeded, the margin was accepted and read back, the pings were
accepted, and nothing would ever have reset the board.

majestic did not notice because it sends WDIOS_ENABLECARD itself right
after opening, which is what re-armed the counter. A client that only
does what the API documents -- busybox watchdog, systemd -- got
nothing.

Measured on a lab hi3516ev300: with a prior magic close, open + margin
30 + die without "V" left the board up indefinitely before this, and
resets it between 26 s and 35 s after.
hi3516cv500 does not build wdt/wdt.c -- hi3516cv500.kbuild points at
wdt/hi3516cv500/hi_wtdg.c, a second copy of the same vendor driver with
the same defects, and the boards it covers (hi3516cv500, hi3516av300)
are affected identically. Measured on a lab hi3516av300 before this:
majestic SIGKILLed at uptime 215043, board still up and counting at
215249.

Carries over, one for one:

  - the margin is the time to the reset. hidog_set_timeout() loaded a
    whole margin into a counter the SP805 runs twice, so every timeout
    meant twice itself.
  - a ping restarts the timer. hidog_feed() returned without touching
    anything unless an interrupt was already pending, and even then only
    cleared it without reloading -- so a ping did nothing until the
    counter had already run out once.
  - an unexpected close leaves the dog to bite. hidog_release() set
    HIDOG_SELFCLR and re-fed, handing the device to the driver's own
    kernel thread at exactly the moment userspace stopped feeding it.
  - opening the device starts the watchdog. hidog_open() only pinged,
    and nothing re-enabled HIWDT_CTRL after a stop.

Plus what the V4 copy also got: a write handler, so the WDIOF_MAGICCLOSE
this has always advertised is reachable at all; WDIOC_GETTIMELEFT;
-EINVAL for an out-of-range margin instead of clamping to one nobody
asked for; SETTIMEOUT reading back the margin in force; -ENOTTY for an
unknown ioctl; and a feeder paced on the margin in force rather than the
module default.

Measured after, on the same board: open, margin 30, die without "V"
resets between 27 s and 36 s, and GETTIMELEFT reports 299 of 300.

Seven more copies of this driver remain in wdt/, for chips with no
hardware here to measure on.
watchdog_exit() ran dog_exit() first and unregistered afterwards.
dog_exit() sets DOG_EXIT and then blocks in kthread_stop() waiting for
the feeder thread to notice -- and the feeder's loop condition is
dog_state, which dog_open() sets straight back to DOG_EXTCLR. With the
device still registered for that whole window, one open() racing the
unload left the thread looping for ever:

  rmmod           D  kthread_stop
                     osal_kthread_destory [open_osal]
                     watchdog_exit [open_wdt]
                     wdg_remove [open_wdt]
                     platform_drv_remove
                     SyS_delete_module

module_mutex held, the task unkillable, and the board did not survive a
clean reboot afterwards -- it needed the power pulled. Hit on a lab
gk7205v200 by nothing more exotic than running a probe against
/dev/watchdog while an rmmod was in flight. Nothing can rescue it once
lost, either: dog_release() leaves dog_state at SELFCLR and dog_open()
at EXTCLR, and neither is DOG_EXIT.

Unregistering first closes the window: after osal_deregisterdevice()
there is no node to open, so nothing can put dog_state back while
dog_exit() waits.

Both copies of the driver have it; both are fixed. Measured on the same
gk7205v200: with a shell loop opening and closing /dev/watchdog as fast
as it can, rmmod now returns 0 in two seconds and the module is gone.
The SP805 watchdog was in the tree ten times: wdt/wdt.c for the V4 parts
and a near-identical copy under wdt/<chip>/ for everyone else, differing
by register base, a prefix on every identifier, and nothing else that
mattered. Six API defects had to be found and fixed twice already.

hi3516cv500 and hi3516cv300 now build wdt/wdt.c, which takes what
actually differs from the kbuild:

  WDT_BASE                       the register map
  WDT_OSAL_HI                    OSAL is hi_osal.h here, not osal.h
  WDT_OSAL_NO_KTHREAD_STOP_FLAG  V3's osal_kthread_destory() takes the
                                 task alone and always stops it
  WDT_NO_OF                      no watchdog node in the device tree
  WDT_SCTL_BASE                  the 3 MHz clock needs ungating first

wdt_init.c grew the second binding style for WDT_NO_OF and now names
the device-tree compatibles outright instead of assembling one from
compat.h, which only knows the two V4 chips. That retires
init/hi3516cv500/wtdg_init.c as well.

hi3516cv300 gains a watchdog it never had. Its copy had no module entry
point at all -- hi3516cv300_wdt.ko loaded, did nothing, and the board
came up with no /dev/watchdog, so majestic logged "Watchdog device
(/dev/watchdog) not found" and every cv300 ran unguarded. Confirmed on
a lab hi3516cv300 before the change; after it the node is there and a
crash resets the board at the margin.

Measured after, all four boards and both binding styles: open, margin
30, die without "V" resets between 26 s and 36 s and the board comes
back; GETTIMELEFT reads 299 of 300; the magic-close write returns 1; a
clean majestic stop resets nothing.

  hi3516cv300   WDT_NO_OF, 0x12080000   (was: no device at all)
  hi3516cv500   platform, 0x12051000    (hi3516av300)
  hi3516ev200   platform, 0x12030000    (hi3516ev300)
  gk7205v200    platform, 0x12030000

Left with their own copy, for reasons that are not cosmetic:

  hi3516cv6xx, hi3519dv500  a later vendor generation -- optional FIQ
    feeding path, an hrtimer, ot_osal.h, command-list ioctl dispatch.
    All six fixes are carried over to both in place instead. There is
    no firmware tree here for either, so those two changes are
    reviewed but neither compiled nor run.

  hi3516cv100, hi3519v101   their kbuild links a vendor .o; the source
    beside it is reference and reaches no board.

  hi3516cv200, hi3516av100, hi3520dv200   V2 has no OSAL at all (the
    osal_v2_shim is a kernel-API shim for blobs, not an OSAL), and the
    driver there is written against plain kernel APIs. Sharing this
    file would mean writing an OSAL for the generation first.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix watchdog reset semantics and consolidate SP805 drivers

🐞 Bug fix ✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Correct SP805 timeout, keepalive, magic-close, reopen, time-left, and unload semantics.
• Consolidate cv300 and cv500 onto shared device-tree and direct-binding drivers.
• Restore hi3516cv300 watchdog registration and document cross-generation configuration.
Diagram

graph TD
  A["Module load"] --> B["SP805 timer"] --> C["Kernel feeder"] --> D["Userspace open"] --> E["Keepalive writes"] --> F{"Close type"}
  F -->|Magic V| G["Timer stopped"]
  F -->|Unexpected| H["Board reset"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Adopt the Linux watchdog framework
  • ➕ Centralizes standard API semantics in the watchdog core
  • ➕ Could reuse the upstream SP805 driver and reduce custom lifecycle code
  • ➕ Improves compatibility with systemd, BusyBox, and standard watchdog tooling
  • ➖ Requires adapters around incompatible vendor OSALs and old kernel trees
  • ➖ Expands scope and regression risk across supported SoC generations
  • ➖ May not fit vendor module packaging and device-tree variations
2. Use runtime SoC platform data
  • ➕ Avoids compile-time feature macros in the shared driver
  • ➕ Keeps register maps and binding capabilities in structured per-SoC data
  • ➖ Requires broader initialization and platform-device restructuring
  • ➖ Offers limited benefit because each target already builds a SoC-specific module
  • ➖ Complicates direct registration on systems without watchdog device-tree nodes

Recommendation: The PR's shared driver with kbuild-selected compatibility flags is the best low-risk backport for these vendor kernels. It removes identical copies while preserving genuinely different later-generation drivers; migration to the Linux watchdog framework should be considered separately rather than combined with this behavioral fix.

Files changed (10) +736 / -184

Enhancement (3) +9 / -1
ot_wtdg.hExpose the time-left watchdog ioctl +1/-0

Expose the time-left watchdog ioctl

• Defines 'WDIOC_GETTIMELEFT' for the cv6xx command-list ioctl implementation.

kernel/wdt/hi3516cv6xx/ot_wtdg.h

ot_wtdg.hExpose the time-left watchdog ioctl +1/-0

Expose the time-left watchdog ioctl

• Defines 'WDIOC_GETTIMELEFT' for the dv500 command-list ioctl implementation.

kernel/wdt/hi3519dv500/ot_wtdg.h

watchdog.hShare the default margin and time-left ioctl definitions +7/-1

Share the default margin and time-left ioctl definitions

• Moves 'DOG_TIMER_MARGIN' into the shared header so initialization code can report it correctly. Enables the standard 'WDIOC_GETTIMELEFT' command.

kernel/wdt/watchdog.h

Bug fix (3) +634 / -172
ot_wtdg.cAlign cv6xx watchdog behavior with the Linux API +191/-41

Align cv6xx watchdog behavior with the Linux API

• Corrects SP805 timeout and ping calculations, implements magic-close writes and 'GETTIMELEFT', and rejects invalid margins. It also makes userspace ownership one-way, restarts the timer on open, paces the feeder from the active margin, and prevents unload/open races.

kernel/wdt/hi3516cv6xx/ot_wtdg.c

ot_wtdg.cAlign dv500 watchdog behavior with the Linux API +191/-42

Align dv500 watchdog behavior with the Linux API

• Ports the corrected SP805 margin, unconditional ping, magic-close, open, ioctl, feeder, and teardown semantics to the dv500-specific driver. Unexpected process closure now leaves the watchdog armed to reset the board.

kernel/wdt/hi3519dv500/ot_wtdg.c

wdt.cImplement correct shared SP805 watchdog lifecycle +252/-89

Implement correct shared SP805 watchdog lifecycle

• Adds cross-generation OSAL and register-map configuration while correcting timeout, keepalive, magic-close, reopen, ioctl, and feeder behavior. It also manages direct mappings and optional clock gating, returns standard errors, and closes the unload/open race.

kernel/wdt/wdt.c

Refactor (1) +46 / -4
wdt_init.cSupport platform and direct watchdog registration +46/-4

Support platform and direct watchdog registration

• Adds module entry points for targets without watchdog device-tree nodes while retaining platform-driver binding. It centralizes all supported HiSilicon and Goke compatible strings and fixes the default-margin parameter description.

kernel/wdt/wdt_init.c

Documentation (1) +37 / -4
readme-eng.txtDocument shared driver architecture and watchdog semantics +37/-4

Document shared driver architecture and watchdog semantics

• Explains supported generations, kbuild feature flags, binding models, and why later-generation drivers remain separate. Documents Linux watchdog API behavior for timeout, keepalive, magic close, and unexpected process termination.

kernel/wdt/readme-eng.txt

Other (2) +10 / -3
hi3516cv300.kbuildBuild cv300 with the shared directly bound watchdog +7/-2

Build cv300 with the shared directly bound watchdog

• Replaces the nonfunctional cv300 driver object with shared 'wdt.c' and 'wdt_init.c'. Supplies the cv300 register base and OSAL/direct-binding compatibility flags so loading the module creates '/dev/watchdog'.

kernel/hi3516cv300.kbuild

hi3516cv500.kbuildBuild cv500 with the shared platform watchdog +3/-1

Build cv500 with the shared platform watchdog

• Replaces the cv500-specific driver and initializer with the shared watchdog implementation. Defines the cv500 register base and HiSilicon OSAL selection.

kernel/hi3516cv500.kbuild

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Failed watchdog loads reboot one board ✓ Resolved 🐞 Bug ☼ Reliability
Description
watchdog_init() routes failed feeder-thread creation to cleanup after dog_init() has already
called dog_start(), but that cleanup never calls dog_stop(). On hi3516cv300, newly selecting
this driver makes an osal_kthread_create() failure leave the hardware armed after module
initialization fails and its mapping is torn down, so the board resets once the configured margin
expires.
Code

kernel/hi3516cv300.kbuild[133]

+$(PREFIX)wdt-objs := wdt/wdt.o wdt/wdt_init.o
Evidence
The kbuild change newly runs the consolidated driver on hi3516cv300. That driver enables the
watchdog before creating the feeder, explicitly returns failure when thread creation fails, and its
resulting cleanup path unmaps and destroys resources without executing the only routine that clears
the watchdog control register.

kernel/hi3516cv300.kbuild[127-134]
kernel/wdt/wdt.c[531-549]
kernel/wdt/wdt.c[630-660]
kernel/wdt/wdt.c[253-274]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly selected hi3516cv300 watchdog starts the hardware before creating its feeder thread, but a thread-creation failure cleans up without disabling the timer. This leaves an armed, unfed watchdog after module initialization returns an error.
## Fix Focus Areas
- kernel/hi3516cv300.kbuild[132-133]
- kernel/wdt/wdt.c[531-549]
- kernel/wdt/wdt.c[630-660]
## Recommended Fix
When `dog_init()` fails after starting the hardware, call `dog_stop()` before unmapping registers or destroying the spinlock, and set the state back to `DOG_EXIT`. Keep the cleanup safe for both kernel-thread and LiteOS thread-creation failures.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reopened watchdogs ignore configured timeout ✓ Resolved 🐞 Bug ≡ Correctness
Description
dog_stop() replaces load_val with the maximum sentinel, while the newly added dog_start() call
in dog_open() only enables the counter and dog_keepalive() reloads that sentinel. After a magic
close or WDIOS_DISABLECARD, reopening the device without another timeout-setting ioctl makes the
watchdog run for roughly the maximum interval rather than the timeout the client previously
configured.
Code

kernel/wdt/wdt.c[R320-321]

+	dog_start();
dog_keepalive();
Evidence
The stop path programs timeout zero, which selects and stores WDT_LOAD_MAX; the changed open path
starts and feeds the device but does not restore cur_margin. Feeding writes load_val directly,
whereas the enable-options path explicitly restores the heartbeat after starting.

kernel/wdt/wdt.c[151-175]
kernel/wdt/wdt.c[253-273]
kernel/wdt/wdt.c[320-321]
kernel/wdt/wdt.c[434-442]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reopening after a deliberate stop arms the watchdog using the maximum sentinel load rather than the previously configured timeout.
### Fix Focus Areas
- kernel/wdt/wdt.c[320-321]
### Recommended Fix
After starting the device in `dog_open()`, restore `cur_margin` with `dog_set_heartbeat(cur_margin)` before the keepalive, or otherwise ensure `dog_stop()` does not leave `load_val` as the active value for the next open.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Newer boards reopen with wrong timeout ✓ Resolved 🐞 Bug ≡ Correctness
Description
ot_dog_stop() stores the maximum sentinel through ot_dog_set_timeout(0), but the new
ot_dog_open() path starts and feeds the hardware without restoring g_cur_margin. A client that
magic-closes or disables the device and later reopens it on either affected later-generation driver
receives a watchdog timeout near the maximum instead of its retained setting.
Code

kernel/wdt/hi3516cv6xx/ot_wtdg.c[R263-264]

+    ot_dog_start();
  ot_dog_keepalive();
Evidence
Both stop functions set timeout zero, whose load helper selects the maximum value and stores it
globally. Both newly changed open paths only start then feed, and feed writes that global value;
their enable-options paths show the required heartbeat restore operation.

kernel/wdt/hi3516cv6xx/ot_wtdg.c[110-140]
kernel/wdt/hi3516cv6xx/ot_wtdg.c[215-230]
kernel/wdt/hi3516cv6xx/ot_wtdg.c[263-264]
kernel/wdt/hi3519dv500/ot_wtdg.c[216-231]
kernel/wdt/hi3519dv500/ot_wtdg.c[264-265]
kernel/wdt/hi3519dv500/ot_wtdg.c[328-336]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Opening after a deliberate stop reloads the maximum sentinel timeout rather than the retained configured timeout in both later-generation watchdog drivers.
### Fix Focus Areas
- kernel/wdt/hi3516cv6xx/ot_wtdg.c[263-264]
- kernel/wdt/hi3519dv500/ot_wtdg.c[264-265]
### Recommended Fix
Call `ot_dog_set_heartbeat(g_cur_margin)` after `ot_dog_start()` in each open handler and before the keepalive, so the load register and saved margin agree.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. A failed write can disable the watchdog ✓ Resolved 🐞 Bug ☼ Reliability
Description
dog_write() sets the global expect_close as soon as it copies a V, then returns -EFAULT
without clearing it if copying a later byte fails. A caller whose write crosses into an invalid
userspace page can close after that failed write and have dog_release() treat the close as
deliberate, stopping the watchdog rather than retaining crash protection.
Code

kernel/wdt/wdt.c[R365-372]

+		expect_close = 0;
+
+		for (i = 0; i != len; i++) {
+			char c;
+			if (osal_copy_from_user(&c, data + i, sizeof(char)))
+				return -EFAULT;
+			if (c == 'V')
+				expect_close = 1;
Evidence
The new write handler clears the flag, sets it on every copied V, and immediately returns on a
later copy error. The release path then calls dog_stop() whenever that stale global flag is set.

kernel/wdt/wdt.c[336-340]
kernel/wdt/wdt.c[357-377]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A partial failed write can leave the magic-close state set even though the write reports an error.
### Fix Focus Areas
- kernel/wdt/wdt.c[365-372]
### Recommended Fix
Track whether `V` was seen in a local variable while copying, return errors without changing `expect_close`, and assign the global flag only after every byte has been copied successfully. Synchronize the state with release if file operations may run concurrently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. A failed write can disable newer watchdogs ✓ Resolved 🐞 Bug ☼ Reliability
Description
Each new ot_dog_write() implementation sets g_expect_close before all of the userspace buffer
has been validated and returns on a later copy fault without rolling the state back. If a write
containing V faults on a following byte, closing the descriptor is then interpreted as a magic
close and stops the watchdog on either later-generation implementation.
Code

kernel/wdt/hi3516cv6xx/ot_wtdg.c[R310-319]

+        g_expect_close = 0;
+
+        for (i = 0; i != size; i++) {
+            char c;
+            if (osal_copy_from_user(&c, buf + i, sizeof(char))) {
+                return -EFAULT;
+            }
+            if (c == 'V') {
+                g_expect_close = 1;
+            }
Evidence
In both copies, the newly added loop sets the global magic-close state before the remaining bytes
are copied, while the release handler stops the timer whenever that state is true. The error return
bypasses the reset that would otherwise clear the state.

kernel/wdt/hi3516cv6xx/ot_wtdg.c[282-285]
kernel/wdt/hi3516cv6xx/ot_wtdg.c[302-325]
kernel/wdt/hi3519dv500/ot_wtdg.c[283-285]
kernel/wdt/hi3519dv500/ot_wtdg.c[303-326]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A partial failed write can retain magic-close permission and make the following close stop the watchdog.
### Fix Focus Areas
- kernel/wdt/hi3516cv6xx/ot_wtdg.c[310-319]
- kernel/wdt/hi3519dv500/ot_wtdg.c[311-320]
### Recommended Fix
Accumulate the magic-close result in a local variable while copying the buffer, commit `g_expect_close` only after the whole copy succeeds, and protect the close state from concurrent write and release operations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread kernel/hi3516cv300.kbuild
Comment thread kernel/wdt/wdt.c Outdated
Comment thread kernel/wdt/hi3516cv6xx/ot_wtdg.c Outdated
Comment thread kernel/wdt/wdt.c
Comment thread kernel/wdt/hi3516cv6xx/ot_wtdg.c
Three things review caught, all of them consequences of the open path
arming the hardware where it previously did not.

dog_stop() parks the load at the counter maximum, because it disarms by
way of dog_set_timeout(0). Starting the device in dog_open() and then
merely pinging it wrote that maximum straight back, so a client that
stopped the watchdog and reopened it without setting the timeout again
got roughly the counter's whole range -- 1431 s at 3 MHz -- while
WDIOC_GETTIMEOUT went on reporting the margin it had asked for. Restore
the heartbeat instead of pinging, which is what the WDIOS_ENABLECARD
path already did for the same reason. Reproduced and fixed on all four
boards:

  before   GETTIMEOUT -> 30, GETTIMELEFT -> 1431
  after    GETTIMEOUT -> 30, GETTIMELEFT -> 29

dog_init() arms the hardware before creating the feeder, and returned
failure on osal_kthread_create() without disarming. watchdog_init() then
unmapped the registers and failed the load, leaving an armed watchdog
with nothing in the system able to feed it: the board resets one margin
later, every boot. It only mattered on paths that reach dog_init() at
all, which hi3516cv300 now does for the first time.

dog_write() set expect_close on the first "V" and could then return
-EFAULT on a later byte with the flag still set, so a write that failed
part way through a buffer crossing into an unmapped page armed a magic
close the caller never completed -- and the next close stopped the
watchdog instead of leaving it to bite. The watchdog core has the same
shape, but here the cost of getting it wrong is a board that quietly
stops being guarded, so clear it on the error path.

All three in wdt.c and in both later-generation copies.
@widgetii

Copy link
Copy Markdown
Member Author

All five findings were real and are fixed in 88528b4. Two of them were mine and my testing had masked them — every probe set a timeout after opening, which is exactly the step that hid the problem.

2 and 3 — reopen ignores the configured timeout. Correct, and the more serious of the five. dog_stop() disarms via dog_set_timeout(0), which parks load_val at the counter maximum; the dog_start() I added to open() then only enabled the counter and the ping wrote that maximum back. So WDIOC_GETTIMEOUT kept reporting the margin the client had asked for while the hardware ran at roughly the whole range. Reproduced on all four lab boards before the fix:

run1: SETTIMEOUT(30) -> 30
run1: stopped with magic close
run2: GETTIMEOUT  -> 30      <- what the client is told
run2: GETTIMELEFT -> 1431    <- what the hardware is doing

open() now restores the heartbeat rather than pinging, which is what the WDIOS_ENABLECARD path already did for the same reason. After, on all four: GETTIMEOUT -> 30, GETTIMELEFT -> 29.

1 — a failed load leaves the dog armed. Correct. dog_init() arms before creating the feeder and returned failure without disarming, so watchdog_init() unmapped the registers and failed the load with the counter still running and nothing in the system able to feed it — a reset one margin later, every boot. dog_stop() on both feeder-creation failure paths. The note that hi3516cv300 newly reaches this code is right; it previously had no module entry point at all.

4 and 5 — expect_close survives a faulting write. Correct that the flag is left set when a later byte faults, so a close after a partly-failed write is taken as deliberate and stops the watchdog. Worth noting the watchdog core has the same shape (watchdog_write() returns -EFAULT mid-loop without clearing _WDOG_ALLOW_RELEASE), so this is a deliberate deviation rather than a straight bug fix — but here the failure mode is a board that quietly stops being guarded, which is worth not inheriting. Cleared on the error path.

All three changes are in wdt.c and in both later-generation copies.

On the alternative approaches in the summary: agreed that migrating to the Linux watchdog framework is the right long-term answer and the wrong thing to combine with a behavioural fix for these vendor kernels. Runtime platform data would also be a reasonable shape, but every target already builds a SoC-specific module, so the compile-time flags cost nothing here and keep the no-device-tree path simple.

One correction to the PR description while I am here: hi3519dv500 does build in CI and passed on 63e6abe, so it is no longer untested. hi3516cv6xx is genuinely not built anywhere — it is not in the CI matrix and there is no firmware tree for it locally — so for that one file I have only a syntax-only compile against its own vendor OSAL and kernel headers, plus the fact that its sibling hi3519dv500 builds from an identical patch. That remains the weakest point of the branch.

@widgetii
widgetii merged commit b922e19 into main Sep 12, 2026
69 of 70 checks passed
@widgetii
widgetii deleted the wdt-linux-api branch September 12, 2026 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Watchdog not working on hi3516ev300 and gk7205v300

1 participant