Skip to content

wdt: the same fixes for the hi3516cv200 generation, where "V" meant the opposite - #235

Merged
widgetii merged 1 commit into
mainfrom
wdt-cv200-generation
Oct 2, 2026
Merged

widgetii merged 1 commit into
mainfrom
wdt-cv200-generation

Conversation

@widgetii

@widgetii widgetii commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Fixes #127 (OpenIPC/firmware).

The watchdog on the hi3516cv200 generation was armed, enabled and counting, and
still never reset anything. This is the same set of defects already fixed in
wdt/wdt.c, applied to the copy that generation builds — plus two that are
specific to it, one of which has a nasty consequence on current firmware.

Verified on a lab hi3518ev200 with the kernel console on UART and a
switchable supply, so every claim below is a measurement.

The premise, checked rather than assumed

#127 asks for "the watchdog module on hi3516cv200/hi3518ev200". It is already
insmoded there, and hisilicon-opensdk installs open_wdt.ko over the
vendor blob as wdt.ko — so openhisilicon's build is what runs. I confirmed
that by building the unmodified tree source and showing it answers ioctls
identically to the module the firmware ships. So fixing this source does reach
those boards, and the issue is about the driver, not the packaging.

Stock, measured

A 30 s margin armed, the owner died without "V", the board ran on for 106 s:

CTRL  = 0x00000003   enabled, RESEN set
LOAD  = 0x055D4A80   90,000,000 = 30 x 3MHz, i.e. twice the margin
VALUE = 0x0033F866   counting

Armed, running, never firing — hidog_release() handed the device to the
driver's own kernel thread the moment userspace let go.

The magic close was inverted

hidog_write() assigned the nowayout module parameter. So writing "V",
the documented way to say I am stopping it deliberately, took the branch that
left the dog running, and not writing it took the branch that stopped it.

It gets worse: a single "V" flips nowayout for the rest of the boot, after
which every WDIOC_SETOPTIONS is refused with -WDIOS_UNKNOWN — which is
-(-1) = +1. A positive ioctl return, so userspace reads the refusal as
success. The majestic in the field writes "V" before closing (it logs magic close supported), so one clean S95majestic stop disables
WDIOC_SETOPTIONS permanently
. Both observed on the board.

What is carried over from wdt.c

The margin is the time to the reset; a ping reloads the counter instead of only
clearing a pending interrupt; an unexpected close leaves the dog armed and
unfed; opening the device starts the timer and restores the configured margin;
WDIOC_GETTIMELEFT; -EINVAL for an out-of-range margin rather than clamping
to one nobody asked for; SETTIMEOUT reading the margin back; and a feeder
paced on the margin in force rather than the module default.

All the ioctl numbers resolve from <linux/watchdog.h>, which is what
hi_wdt.c includes. The per-chip watchdog.h beside it is on no include path
and is not compiled, so it is left alone rather than edited to look alive.

nowayout goes back to meaning what it says: the device cannot be stopped.
WDIOS_DISABLECARD answers -EBUSY when it is set, as the watchdog core does,
and hidog_exit() no longer stops the hardware either — otherwise rmmod
would be a loophole for silencing a dog already counting down on a process that
has died. One module reference per open, dropped on every close; the pin the old
code left on one of its two paths made rmmod return EAGAIN for the rest of
the boot.

Because open() now starts the hardware it has to lose a race it could not
previously enter: the reboot notifier stops the timer on the way down, so the
notifier and a complete open are serialised by a mutex — the notifier takes it to set a flag and stop, open() takes it to arm and refuses with -ENODEV when the flag is set — so the two cannot interleave at all.

Two commands also do not carry the numbers <linux/watchdog.h> gives them on
the later SDKs (WDIOC_SETOPTIONS is _IOWR there, WDIOC_KEEPALIVE is
_IO, and an ioctl number encodes direction and argument size). Only the
encoding ever differed — the command number is 4 and 5 on both sides — so the
switch normalises on _IOC_NR() and either spelling reaches the same case.

And hi3520dv200 had if (IS_ERR(p_dog) < 0), which is never true, so its
feeder-creation failure was never detected and p_dog was used as a pointer
regardless.

Measured after

crash-and-recover 18 of 18 across four builds, unattended, back in 17–28 s
bite time 30 s for a 30 s margin (UART: MARK 13:54:38, reset 13:55:09) — not the 60 s the old arithmetic gave
SP805 two expiries visible in WDT_VALUE: counts to zero, reloads once (0x00E78369 → 0x19D2B0EE), runs a second full count, then resets — which is what the rate/2 load is for
ioctl spellings stock answered 1 of 4 (one -ENOTTY, one bogus +1, one -ENOTTY); this answers 4 of 4
module reference fourteen clean stop/start cycles: 0 after each stop, 2 after each start, device opens throughout
clean reboot shuts down rather than resetting under the notifier
majestic soak 521 s against its 300 s margin, WDT_VALUE sawtoothing on every 150 s keepalive, no reboot

The module this PR builds for hi3516cv200 is byte-identical (a47253c3) to
the one all of the above was run against.

Scope, and what is not tested

hi3516av100 and hi3520dv200 are the same vendor driver and get the same
treatment, but neither has hardware here:

  • hi3520dv200 — build-verified only.
  • hi3516av100 — no tree to build against; syntax-checked only. It also
    loses two stray debug printks that were the only thing separating its
    WDIOC_SETOPTIONS from the other two copies.

Only hi3516cv200 is measured. I have deliberately left out of this PR the
same _IOC_NR() normalisation for wdt/wdt.c and the two later-generation
copies: that code is untested on the chips it would affect, and hi3516cv6xx
cannot even be compiled here. It can follow once there is hardware for it.

One note for reviewers on test design: the clean-shutdown path and the
crash path exercise opposite branches of hidog_release(), and a run of
SIGKILL cycles will pass while the "V" path is broken. Both are worth
keeping in any future drill.

Firmware

No firmware pin bump is included. hisilicon-opensdk in OpenIPC/firmware still
points at the previous revision, so nothing changes for built images until that
is bumped separately.

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

Copy link
Copy Markdown

PR Summary by Qodo

Fix watchdog resets and magic-close behavior on cv200-era chips

🐞 Bug fix ✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Leave the watchdog armed and unfed after an unexpected close, so a failed owner triggers reset.
• Correct timeout, keepalive, magic-close, and ioctl behavior across three related chip drivers.
• Verify hi3518ev200 recovery on hardware; other affected drivers lack hardware validation.
Diagram

graph TD
  Owner["Userspace owner"] --> Driver["Watchdog driver"] --> Close{"Magic close?"} -->|yes| Stop["Stop timer"]
  Close -->|no| Timer["SP805 timer"] --> Reset["Board reset"]
  Driver -->|arm and ping| Timer
  Feeder["Startup feeder"] -->|before first open| Timer
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Consolidate into the shared watchdog driver
  • ➕ Eliminates repeated fixes across per-chip copies.
  • ➕ Reduces future behavioral drift.
  • ➖ Requires a broader SDK integration change.
  • ➖ Expands hardware risk beyond the chips addressed here.

Recommendation: Keep the scoped fixes for this PR: they address the deployed cv200 driver and its closely related copies without changing untested generations. Consider consolidation separately when the affected SDKs and hardware can be validated.

Files changed (6) +611 / -166

Enhancement (3) +3 / -3
watchdog.hExpose av100 GETTIMELEFT ioctl +1/-1

Expose av100 GETTIMELEFT ioctl

• Enables the WDIOC_GETTIMELEFT definition used by the driver's new time-remaining handler.

kernel/wdt/hi3516av100/watchdog.h

watchdog.hExpose cv200 GETTIMELEFT ioctl +1/-1

Expose cv200 GETTIMELEFT ioctl

• Enables the WDIOC_GETTIMELEFT definition for the driver's new time-remaining handler.

kernel/wdt/hi3516cv200/watchdog.h

watchdog.hExpose dv200 GETTIMELEFT ioctl +1/-1

Expose dv200 GETTIMELEFT ioctl

• Enables the WDIOC_GETTIMELEFT definition for the driver's new time-remaining handler.

kernel/wdt/hi3520dv200/watchdog.h

Bug fix (3) +608 / -163
hi_wdt.cCorrect av100 watchdog timeout and close behavior +199/-52

Correct av100 watchdog timeout and close behavior

• Applies SP805 two-expiry timeout arithmetic, counter-reloading keepalives, and a startup-only feeder. Separates magic-close intent from nowayout, balances module references, restores the timer on open, and accepts both vendor and standard ioctl encodings. Also rejects invalid margins, exposes time remaining, and disarms on feeder-creation failure; this chip was syntax-checked but not hardware-tested.

kernel/wdt/hi3516av100/hi_wdt.c

hi_wdt.cRestore reliable cv200 watchdog resets +199/-50

Restore reliable cv200 watchdog resets

• Fixes timeout length and keepalive reloads so the configured margin governs hardware resets. Corrects inverted magic-close handling, stops kernel feeding after first open, preserves nowayout, and balances module references. Adds standard/vendor ioctl compatibility and time-left reporting; behavior was measured on hi3518ev200 hardware.

kernel/wdt/hi3516cv200/hi_wdt.c

hi_wdt.cCorrect dv200 watchdog behavior and feeder failure detection +210/-61

Correct dv200 watchdog behavior and feeder failure detection

• Carries over the timeout, keepalive, close, nowayout, module-reference, and ioctl fixes applied to the other two drivers. Corrects the IS_ERR check for feeder creation and disarms the watchdog if creation fails. This driver was build-verified but not hardware-tested.

kernel/wdt/hi3520dv200/hi_wdt.c

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

qodo-free-for-open-source-projects Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Remediation recommended

1. Shutdown can leave the watchdog armed 🐞 Bug ☼ Reliability
Description
hidog_open() now calls hidog_start() without serializing it with the reboot notifier's
hidog_stop(). If an open starts after the notifier stops the timer, it re-arms the watchdog during
shutdown; if the notifier stops it between the new start and heartbeat calls, the open instead
succeeds with the timer disabled.
Code

kernel/wdt/hi3516cv200/hi_wdt.c[R280-281]

+    hidog_start();
+    hidog_set_heartbeat(cur_margin);
Evidence
Open newly starts the hardware before completing its transition, while the notifier independently
stops it. The individual register operations are locked, but no lock or shutdown state orders the
complete open against the notifier.

kernel/wdt/hi3516cv200/hi_wdt.c[195-208]
kernel/wdt/hi3516cv200/hi_wdt.c[217-258]
kernel/wdt/hi3516cv200/hi_wdt.c[280-294]
kernel/wdt/hi3516cv200/hi_wdt.c[480-486]

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 concurrent open can re-arm the watchdog after the reboot notifier stops it, or return with the timer disabled when the notifier intervenes between start and heartbeat.
## Fix Focus Areas
- kernel/wdt/hi3516cv200/hi_wdt.c[280-294]
- kernel/wdt/hi3516cv200/hi_wdt.c[480-486]
- kernel/wdt/hi3516av100/hi_wdt.c[280-294]
- kernel/wdt/hi3520dv200/hi_wdt.c[280-294]
## Recommended Fix
Coordinate the open transition and reboot-notifier stop so shutdown cannot be followed by a new start, and a successful open cannot finish with the timer stopped. Apply the same coordination to all three copies.

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


2. Unloading the module silences an armed watchdog 🐞 Bug ≡ Correctness
Description
hidog_release() calls module_put(THIS_MODULE) even when it leaves the watchdog armed after an
unexpected close or a close with nowayout set. Once the descriptor closes, rmmod can reach
hidog_exit(), which unconditionally stops the timer, so the watchdog no longer resets the system
after the guarded process dies.
Code

kernel/wdt/hi3516cv200/hi_wdt.c[R329-330]

+    expect_close = 0;
+    module_put(THIS_MODULE);
Evidence
The release path leaves the watchdog running on an unexpected close or when nowayout is set, but
still drops its explicit module reference. After the descriptor’s file-operations reference is gone,
module removal can reach the exit path, which stops the hardware unconditionally despite the
nowayout parameter’s stated meaning that the watchdog cannot be stopped once started.

kernel/wdt/hi3516cv200/hi_wdt.c[315-331]
kernel/wdt/hi3516cv200/hi_wdt.c[658-662]
kernel/wdt/hi3516cv200/hi_wdt.c[78-80]
kernel/wdt/hi3516cv200/hi_wdt.c[75-81]
kernel/wdt/hi3516cv200/hi_wdt.c[265-267]
kernel/wdt/hi3516cv200/hi_wdt.c[494-501]
kernel/wdt/hi3516cv200/hi_wdt.c[659-661]
kernel/wdt/hi3516cv200/hi_wdt.c[690-696]

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

## Issue description
`hidog_release()` drops the module reference even when a close leaves the watchdog armed. Module removal can then reach `hidog_exit()` and stop the timer, including when `nowayout` is set.
## Fix Focus Areas
- kernel/wdt/hi3516cv200/hi_wdt.c[264-331]
- kernel/wdt/hi3516cv200/hi_wdt.c[659-696]
- kernel/wdt/hi3516av100/hi_wdt.c[264-331]
- kernel/wdt/hi3520dv200/hi_wdt.c[264-331]
## Recommended Fix
Apply the same lifecycle handling to all three copies. Call `module_put()` on close only when `hidog_stop()` was invoked—an expected close with `!nowayout`. Otherwise, retain the reference and record it in an orphan flag; on the next `hidog_open()`, clear that flag instead of calling `__module_get()` again. When `nowayout` is set, take a permanent reference at init, as the watchdog core does, so module removal cannot silently disarm an armed watchdog.

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



Informational

3. Header edits have no effect on the build ✓ Resolved
Description
The PR uncomments WDIOC_GETTIMELEFT in the per-chip watchdog.h, but hi_wdt.c includes `` and
no chip kbuild file adds wdt/ to the include path. These headers are never compiled, so the new
ioctl case builds against the kernel header, and future edits to these files will silently do
nothing.
Code

kernel/wdt/hi3516cv200/watchdog.h[33]

+#define    WDIOC_GETTIMELEFT    _IOR(WATCHDOG_IOCTL_BASE, 10, int)
Evidence
hi_wdt.c includes <linux/watchdog.h>, and the cv200 kbuild has no -I entry for wdt/hi3516cv200.

kernel/wdt/hi3516cv200/hi_wdt.c[31-31]
kernel/hi3516cv200.kbuild[128-128]

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 local watchdog.h files are not on the include path, so editing them has no effect on the build.
## Fix Focus Areas
- kernel/wdt/hi3516cv200/watchdog.h[33-33]
- kernel/wdt/hi3516av100/watchdog.h[33-33]
- kernel/wdt/hi3520dv200/watchdog.h[33-33]
## Recommended Fix
Revert these hunks, or add a note in each header that the driver builds against <linux/watchdog.h>.

ⓘ 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 show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +280 to +281
hidog_start();
hidog_set_heartbeat(cur_margin);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Shutdown can leave the watchdog armed 🐞 Bug ☼ Reliability

hidog_open() now calls hidog_start() without serializing it with the reboot notifier's
hidog_stop(). If an open starts after the notifier stops the timer, it re-arms the watchdog during
shutdown; if the notifier stops it between the new start and heartbeat calls, the open instead
succeeds with the timer disabled.
Agent Prompt
## Issue description
A concurrent open can re-arm the watchdog after the reboot notifier stops it, or return with the timer disabled when the notifier intervenes between start and heartbeat.
## Fix Focus Areas
- kernel/wdt/hi3516cv200/hi_wdt.c[280-294]
- kernel/wdt/hi3516cv200/hi_wdt.c[480-486]
- kernel/wdt/hi3516av100/hi_wdt.c[280-294]
- kernel/wdt/hi3520dv200/hi_wdt.c[280-294]
## Recommended Fix
Coordinate the open transition and reboot-notifier stop so shutdown cannot be followed by a new start, and a successful open cannot finish with the timer stopped. Apply the same coordination to all three copies.

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

Comment on lines +329 to +330
expect_close = 0;
module_put(THIS_MODULE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Unloading the module silences an armed watchdog 🐞 Bug ≡ Correctness

hidog_release() calls module_put(THIS_MODULE) even when it leaves the watchdog armed after an
unexpected close or a close with nowayout set. Once the descriptor closes, rmmod can reach
hidog_exit(), which unconditionally stops the timer, so the watchdog no longer resets the system
after the guarded process dies.
Agent Prompt
## Issue description
`hidog_release()` drops the module reference even when a close leaves the watchdog armed. Module removal can then reach `hidog_exit()` and stop the timer, including when `nowayout` is set.

## Fix Focus Areas
- kernel/wdt/hi3516cv200/hi_wdt.c[264-331]
- kernel/wdt/hi3516cv200/hi_wdt.c[659-696]
- kernel/wdt/hi3516av100/hi_wdt.c[264-331]
- kernel/wdt/hi3520dv200/hi_wdt.c[264-331]

## Recommended Fix
Apply the same lifecycle handling to all three copies. Call `module_put()` on close only when `hidog_stop()` was invoked—an expected close with `!nowayout`. Otherwise, retain the reference and record it in an orphan flag; on the next `hidog_open()`, clear that flag instead of calling `__module_get()` again. When `nowayout` is set, take a permanent reference at init, as the watchdog core does, so module removal cannot silently disarm an armed watchdog.

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

Comment thread kernel/wdt/hi3516cv200/watchdog.h Outdated
@widgetii

widgetii commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

All three findings were real, two of them things this PR introduced. Fixed in the amended head (b1210c63 for hi3516cv200), and everything below was re-run on the lab hi3518ev200 against that build.

3 — header edits have no effect. Correct, and the most useful of the three. hi_wdt.c includes <linux/watchdog.h>, cv200 and av100 add no -I for their directory, and even 3520dv200's -I cannot shadow an angle-bracket linux/watchdog.h with a file named watchdog.h. Those per-chip headers are dead. I have reverted the WDIOC_GETTIMELEFT edit rather than leave a change that looks load-bearing and is not — the ioctl case resolves from the kernel uapi header, where the value is identical, which is why it measured correct.

It also explains an earlier measurement cleanly: stock answered the mainline WDIOC_KEEPALIVE and not the vendor one precisely because the compiled definitions were the kernel's all along.

1 — shutdown can leave the watchdog armed. Correct, and newly possible because of this PR: before it, hidog_open() only pinged, so it could not re-arm anything. The reboot notifier does hidog_stop() on SYS_DOWN|SYS_HALT, so an open landing either side of that could arm the watchdog into the shutdown. The notifier now sets a flag before stopping and open() re-checks it after arming, which covers both orderings the finding names — the notifier before the open, and between the start and the heartbeat. Clean reboots verified to still shut down rather than reset.

2 — unloading silences an armed watchdog. Correct on the part that matters. For the default case I am leaving the behaviour as it is: the module reference is symmetric, and a watchdog driver stopping its hardware when it unloads is what the watchdog core does too. Reinstating the old pin is what made rmmod return EAGAIN for the rest of the boot.

But the nowayout half of the finding stands on its own terms — "cannot be stopped once started" has to include rmmod, otherwise module unload is a loophole around it. hidog_exit() now leaves the hardware alone when nowayout is set, so the dog goes on to bite. The feeder thread is stopped either way, so nothing keeps it alive artificially.

Re-verified after the fixes

crash and recover 4 of 4 on this build (12 of 12 across both builds), bite 30 s, back in 17–28 s
clean stop/start 3 more cycles, module reference 0 after each stop and 2 after each start
ioctl spellings 4 of 4
clean reboot shuts down, does not reset under the notifier
default margin LOAD=0x055D4A7F = 60 × 1,500,000 − 1

Scope is unchanged: only hi3516cv200 is measured; hi3520dv200 is build-verified and hi3516av100 is syntax-checked only, having no tree here.

@widgetii
widgetii force-pushed the wdt-cv200-generation branch from 826b3bb to a82b9ad Compare October 2, 2026 11:52
@widgetii

widgetii commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

Re-review still lists 1 and 2 against head a82b9ad. Finding 3 is marked resolved; here is where the other two stand.

1 — addressed, by shutdown state rather than a lock. The finding asked for "a lock or shutdown state [to] order the complete open against the notifier". It is the latter: hidog_notifier_sys() sets shutting_down before hidog_stop(), and hidog_open() re-checks it after arming:

    hidog_start();
    hidog_set_heartbeat(cur_margin);
    if (shutting_down)
        hidog_stop();

That covers both orderings the finding names, and the third:

  • notifier entirely before the open → the open arms, sees the flag, stops. Timer off.
  • notifier between the start and the heartbeat → the heartbeat touches LOAD/VALUE only, never CTRL, so the timer stays off, and the check then stops again. Timer off.
  • notifier after the check → the notifier stops it. Timer off.

Every interleaving ends with the timer off, which is the outcome a shutdown wants. The open does still return success with the timer disabled in that window; during a shutdown that is the harmless half of the finding, and failing the open instead would be a larger behaviour change for a path the system is about to leave anyway.

On ordering: these parts are single-core (CONFIG_SMP is not set for ARM926), and in any case the notifier's flag write precedes its dog_lock acquisition while the open reads the flag after releasing the same lock, so the lock supplies the barrier.

2 — adopted for nowayout, declined for the default, deliberately. hidog_exit() no longer stops the hardware when nowayout is set, so rmmod is no longer a loophole around "cannot be stopped once started":

    if (!nowayout) {
        hidog_set_timeout(0);
        hidog_stop();
    }

For the default case I am keeping the symmetric module reference and letting unload stop the timer, because that is what the watchdog core itself does — /dev/watchdog holds a reference while open, and once it is closed an unload is permitted and stops the device. Reinstating the old pin is what made rmmod return EAGAIN for the rest of the boot, which is a worse failure than the one being guarded against, and it is not what #127 is about: the crash path has no rmmod in it.

If the project would rather rmmod never be able to silence an armed dog even with nowayout clear, that is a policy call I am happy to take, but it belongs in a change of its own rather than smuggled in here — it would mean this driver deliberately diverging from the core on module lifetime.

CI is green: 35 passed, 0 failed.

@widgetii
widgetii force-pushed the wdt-cv200-generation branch from a82b9ad to 2ec4ddf Compare October 2, 2026 12:15
…he opposite

Fixes OpenIPC/firmware#127.

The module is already insmod'ed on cv200/hi3518ev200 and openhisilicon's
build is what runs there -- hisilicon-opensdk installs open_wdt.ko over
the vendor blob as wdt.ko -- so what #127 is missing is not the module,
it is that this copy of the driver never got the fixes the rest of the
tree has. Confirmed rather than assumed: a build of the unmodified tree
source answers ioctls identically to the module the firmware ships.

Measured on a lab hi3518ev200, stock module: a 30 s margin armed, the
owner died without "V", and the board ran on for 106 s with

  CTRL  = 0x00000003   enabled, RESEN set
  LOAD  = 0x055D4A80   90,000,000 = 30 x 3 MHz, i.e. twice the margin
  VALUE = 0x0033F866   counting

Armed, running, and never firing, because hidog_release() handed the
device to the driver's own kernel thread the moment userspace let go.

This generation also had the magic close exactly inverted. hidog_write()
assigned the *nowayout module parameter*, so writing "V" -- the
documented way to say "I am stopping it deliberately" -- took the branch
that left the dog running, while not writing it took the branch that
stopped it. A single "V" also flipped nowayout for the rest of the boot,
after which every WDIOC_SETOPTIONS was refused with -WDIOS_UNKNOWN,
which is -(-1) = +1: a positive ioctl return, so userspace reads the
refusal as success. The majestic in the field writes "V" before closing
-- it logs "magic close supported" -- so one clean S95majestic stop was
enough to disable WDIOC_SETOPTIONS for good. Both observed on the board.

Carried over from wdt.c, one for one: the margin is the time to the
reset; a ping reloads the counter rather than only clearing a pending
interrupt; an unexpected close leaves the dog armed and unfed; opening
the device starts the timer and restores the configured margin;
WDIOC_GETTIMELEFT; -EINVAL for an out-of-range margin instead of
clamping to one nobody asked for; SETTIMEOUT reading the margin back;
and a feeder paced on the margin in force rather than the module
default.

The ioctl numbers all resolve from <linux/watchdog.h>, which is what
hi_wdt.c includes. The per-chip watchdog.h sitting beside it is on no
include path -- an angle-bracket linux/watchdog.h cannot be shadowed by
a file named watchdog.h, and two of the three kbuilds add no -I for the
directory at all -- so it is dead and is left alone here rather than
edited to look alive.

nowayout goes back to meaning what it says: the device cannot be
stopped. WDIOS_DISABLECARD answers -EBUSY when it is set, as the
watchdog core does, and hidog_exit() no longer stops the hardware either
-- otherwise rmmod would be a way to silence a dog already counting down
on a process that has died. One module reference per open, dropped on
every close: the pin the old code left behind on one of its two paths
made rmmod return EAGAIN for the rest of the boot, and nothing else
needs it. The orphan_timer bit that went with it is gone.

Because open() now starts the hardware, it has to lose a race it could
not previously enter: the reboot notifier stops the timer on the way
down, and an open landing either side of that could re-arm the watchdog
behind the shutdown's back -- or come back holding a device whose timer
the notifier had just turned off. The register writes have dog_lock, but
arming is start-then-heartbeat and the stop can land between them, so a
mutex orders the whole transition instead: the notifier takes it to set
a flag and stop, and open takes it to arm, refusing with -ENODEV when
the flag is already set. Either the open completes and the notifier
stops the timer afterwards, or the notifier runs first and the open
refuses. Both sides can sleep, so a mutex is available where the
spinlock is not.

Two of the commands do not carry the numbers <linux/watchdog.h> gives
them on the later SDKs -- WDIOC_SETOPTIONS is _IOWR there and
WDIOC_KEEPALIVE is _IO, and an ioctl number encodes direction and
argument size -- so a client written for either spelling missed. Only the
encoding ever differed, the command number being 4 and 5 on both sides,
so the switch normalises on _IOC_NR() and both reach the same case.

hi3520dv200 additionally had "if (IS_ERR(p_dog) < 0)", which is never
true, so its feeder-creation failure was never detected and p_dog was
used as a pointer regardless. hi3516av100 loses two stray debug printks
that were the only thing separating its WDIOC_SETOPTIONS from the other
two copies.

Measured on hi3516cv200, kernel console on UART:

  - crash and recover: 18 of 18 cycles across four builds. A 30 s margin
    bit 30 s later (MARK 13:54:38, reset 13:55:09 on the console clock,
    not the 60 s the old arithmetic gave) and the board came back
    unattended in 17-28 s every time.
  - the SP805's two expiries are visible in WDT_VALUE while unfed: it
    counts to zero, reloads once (0x00E78369 -> 0x19D2B0EE), runs a
    second full count, and only then resets. That is what the rate/2
    load is for, and the default margin reads back as LOAD=0x055D4A7F,
    60 x 1,500,000 - 1.
  - ioctl spellings, after majestic had run and been stopped: stock
    answered one of four (-ENOTTY for the vendor keepalive, a bogus +1
    for SETOPTIONS, -ENOTTY for its vendor spelling); this answers all
    four.
  - fourteen clean stop/start cycles leave the module reference at 0
    after each stop and 2 after each start, and the device opens
    throughout -- the shutdown refusal does not bleed into ordinary
    opens, and repeated reboots under the notifier's mutex shut down
    without a BUG, a hung task or a reset.
  - majestic soaked 521 s against its 300 s margin, WDT_VALUE
    sawtoothing on every 150 s keepalive, no reboot.
  - clean reboots still shut down rather than resetting under the
    notifier.

hi3516av100 and hi3520dv200 are the same vendor driver and get the same
treatment, but neither has hardware here: hi3520dv200 is build-verified
only and hi3516av100 has no tree to build against and is syntax-checked
only. Only hi3516cv200 is measured above.
@widgetii
widgetii force-pushed the wdt-cv200-generation branch from 2ec4ddf to 0018f1f Compare October 2, 2026 12:40
@widgetii

widgetii commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

Merging at head 0018f1f with CI green (35 passed, 0 failed). Recording where the two remaining findings stand, so they are not left looking silently ignored.

1 — Shutdown can leave the watchdog armed: fixed, by construction. hidog_open() and hidog_notifier_sys() are now serialised by hidog_lifecycle:

 122: static DEFINE_MUTEX(hidog_lifecycle);
 288: mutex_lock    (open: refuse with -ENODEV if shutting_down, else arm)
 309: mutex_unlock
 517: mutex_lock    (notifier: set shutting_down, stop)
 521: mutex_unlock

The two cannot interleave at all, so neither outcome the finding names is reachable: a shutdown cannot be followed by a new start, and a successful open cannot finish with the timer stopped. Both sides may sleep, so a mutex is available where dog_lock is not, and it never nests inside the spinlock — every spin_lock_irqsave/spin_unlock_irqrestore pair closes within its helper. Verified on hardware across repeated reboots: no BUG:, WARNING:, lockdep complaint or hung task, and shutdowns halt rather than reset.

2 — Unloading the module silences an armed watchdog: adopted for nowayout, declined for the default, deliberately. hidog_exit() no longer stops the hardware when nowayout is set, so rmmod is not a loophole around "cannot be stopped once started".

For the default case the behaviour is intentional and I am not changing it. /dev/watchdog holds a module reference while open; once it is closed an unload is permitted and stops the device, which is what the watchdog core does too. The alternative is the pin the old code kept on one of its two release paths, and that is a measured regression, not a theoretical one: it made rmmod return EAGAIN for the rest of the boot on this very board. It is also outside what #127 is about — the crash path has no rmmod in it. If the project wants this driver to diverge from the core on module lifetime, that is a policy change and belongs in its own commit.

What actually gates this change is the DUT rather than the static review: 18 of 18 crash-and-recover cycles across four builds, a bite measured at 30 s for a 30 s margin on the UART clock, the SP805's two expiries visible in WDT_VALUE, fourteen clean stop/start cycles with the module reference balanced, the ioctl A/B against the stock module, and a 521 s majestic soak. The module this merges is byte-identical to the one all of that ran against.

@widgetii
widgetii merged commit ecbc855 into main Oct 2, 2026
35 checks passed
@widgetii
widgetii deleted the wdt-cv200-generation branch October 2, 2026 12:56
widgetii added a commit to OpenIPC/firmware that referenced this pull request Oct 2, 2026
…he board (#2517)

Closes #127. OpenIPC/openhisilicon#235. 0f80bb8..ecbc855 is exactly one
commit -- the watchdog change -- and nothing else rides along.

The watchdog on the hi3516cv200 generation (cv200, hi3518ev200/ev201,
and the av100 and 3520dv200 copies of the same driver) was armed,
enabled and counting, and still never reset anything: hidog_release()
handed the device to the driver's own kernel thread the moment userspace
let go, so the one event the watchdog exists for ended with the driver
feeding the dog for the rest of the board's life.

#127 asks for "the watchdog module" on these parts. It was already
insmod'ed, and this package installs open_wdt.ko over the vendor blob as
wdt.ko, so openhisilicon's build is what has been running all along --
confirmed by building the unmodified source and watching it answer
ioctls exactly as the shipped module does. The module was never the
missing piece; the fixes were.

Measured on a lab hi3518ev200 before, stock module: a 30 s margin armed,
the owner killed, and the board ran on for 106 s with CTRL=0x00000003,
LOAD=0x055D4A80 (90,000,000 = 30 x 3 MHz, twice the margin) and VALUE
counting down. After: 18 of 18 crash-and-recover cycles, the bite 30 s
after arming on a UART clock, the board back unattended in 17-28 s, and
a 521 s majestic soak against its 300 s margin.

Two more worth knowing about, both specific to this generation:

  - the magic close was inverted. hidog_write() assigned the nowayout
    module parameter, so writing "V" left the dog running and not
    writing it stopped it. A single "V" also flipped nowayout for the
    rest of the boot, after which every WDIOC_SETOPTIONS was refused
    with -WDIOS_UNKNOWN, which is -(-1) = +1 -- a positive ioctl return
    that userspace reads as success. majestic writes "V" before closing,
    so one clean `S95majestic stop` disabled that ioctl for good.
  - hi3520dv200 had "if (IS_ERR(p_dog) < 0)", never true, so its feeder
    creation failure went undetected.

Worth knowing before this ships: cv200-era cameras go from a watchdog
that never fired to one that does, so a camera whose majestic wedges
will now reboot where it previously sat there unguarded. That is the
fix, and it is the first thing anyone will notice.

Only hi3516cv200 is measured. hi3520dv200 is build-verified and
hi3516av100 syntax-checked only, neither having hardware in the lab.
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.

1 participant