// SPDX-License-Identifier: GPL-2.0+ /* Copyright (c) 2018-2019 AVM GmbH */ #include #include #include #include #include #include #include #include #include #include #include "avm_sammel.h" #define DRV_NAME "ar7wdt_hw_shim" enum wdt_target { AR7WDT_TARGET_READY = 0, AR7WDT_TARGET_ACTIVE, AR7WDT_TARGET_DISABLED }; struct ar7wdt_shim_info { struct watchdog_device *watchdog; enum wdt_target wdt_target; struct notifier_block wdt_device_notifier; struct mutex lock; }; static struct ar7wdt_shim_info ar7wdt_shim_info = { .lock = __MUTEX_INITIALIZER(ar7wdt_shim_info.lock), }; static atomic64_t watchdog_touch_ts; static u64 watchdog_bark_delay __read_mostly; static int watchdog_cpu __read_mostly; static void hit_watchdog_snooze_button(void) { u64 now; /* * Order is important here. First re-arm the timer, only then appease * the nmi handler, otherwise there would be a window with no watchdog * protection. * * Disable preemption so that we get a reasonably accurate timestamp, * and record the timestamp before arming the timer, so that the point * in time being recorded is guaranteed to be earlier than the one * used when programming the timer h/w, and a deviation is harmless. */ preempt_disable(); now = cpu_clock(watchdog_cpu); avm_nmi_timer_update(); smp_mb__before_atomic(); atomic64_set(&watchdog_touch_ts, now); preempt_enable(); } static void watchdog_nmi_configure_bark(unsigned long nmi_delay) { atomic64_set(&watchdog_touch_ts, cpu_clock(watchdog_cpu)); watchdog_bark_delay = (u64)nmi_delay * NSEC_PER_SEC; } /* A 64-bit clone of time_after() */ static bool clock_after(u64 a, u64 b) { return (s64)(b - a) < 0; } /* * Since the nmi timer is a hpet and manipulating hpet h/w is slow, we don't * query the hpet but use the cpu-local clock to estimate if the hpet timer * has fired and the watchdog should bark. * * This can't be exact, but we can tolerate some deviation that results in * false positives around the expected timer firing time, if only there are no * false negatives which would lead to watchdog failure. */ static bool watchdog_should_bark(void) { u64 touch_ts, bark_after, now; bool should_bark; touch_ts = atomic64_read(&watchdog_touch_ts); bark_after = touch_ts + watchdog_bark_delay; now = local_clock(); /* Add a safety margin rather than fully trust the timer */ now += NSEC_PER_SEC / 2; should_bark = clock_after(now, bark_after); if (should_bark) pr_devel("%s: now=%llu, bark_after=%llu, should_bark=%d\n", __func__, now, bark_after, should_bark); return should_bark; } static int nmi_timer_handler(unsigned int cmd, struct pt_regs *regs) { static atomic_t lock = ATOMIC_INIT(0); if (watchdog_should_bark() && !atomic_xchg(&lock, 1)) nmi_exception(regs); return NMI_DONE; } static int watchdog_nmi_enable(void) { int result, nmi_delay; nmi_delay = 5 * WDT_DEFAULT_TIME; result = register_nmi_handler(NMI_LOCAL, nmi_timer_handler, 0, "nmi_wd"); if (result != 0) { pr_err("[%s] register_nmi_handler() failed\n", __func__); return result; } /* * The watchdog uses a cpu-local clock; be careful to set up the * watchdog and the timekeeping on the same cpu. TODO: Handle this * cpu going offline. */ watchdog_cpu = smp_processor_id(); watchdog_nmi_configure_bark(nmi_delay); result = avm_nmi_timer_start(nmi_delay, cpumask_of(watchdog_cpu)); if (result != 0) { pr_err("[%s] avm_nmi_timer_start() failed\n", __func__); unregister_nmi_handler(NMI_LOCAL, "nmi_wd"); } return result; } static void watchdog_nmi_disable(void) { avm_nmi_timer_stop(); unregister_nmi_handler(NMI_LOCAL, "nmi_wd"); } static int nmi_notify(struct notifier_block *self, unsigned long unused, void *unused2) { struct cpu_context *ctx; int this_cpu, cpu; this_cpu = smp_processor_id(); avm_set_reset_status(RS_NMIWATCHDOG); oops_enter(); add_taint(TAINT_DIE, LOCKDEP_NOW_UNRELIABLE); printk_avm_console_bend(0); /* force serial-output */ console_verbose(); bust_spinlocks(1); avm_stack_check(NULL); ctx = nmi_get_context(this_cpu); if (!ctx->task) { pr_emerg("No context for CPU#%d which raised the exception?!\n", this_cpu); } else { avm_stack_check(ctx->task); show_task_regs(ctx->task, ctx->regs); } if (num_online_cpus() > 1) pr_emerg("All other CPUs:\n"); for_each_online_cpu(cpu) { if (cpu == this_cpu) continue; ctx = nmi_get_context(cpu); if (!ctx->task) { pr_emerg("No context for CPU#%d\n", cpu); continue; } avm_stack_check(ctx->task); show_task_regs(ctx->task, ctx->regs); } avm_oom_show_memstat(0x1); /* oops_exit triggers kmsg_dump() -> last chance to get infos for panic-log */ oops_exit(); panic("Fatal exception: NMI"); return NOTIFY_STOP; } static struct notifier_block nmi_nb = { .notifier_call = nmi_notify, .priority = INT_MAX }; static struct ar7wdt_shim_info *ar7wdt_hw_get_shim(void) { return &ar7wdt_shim_info; } static int ar7wdt_hw_shim_get_device(struct ar7wdt_shim_info *shim, struct watchdog_device *wdd) { /* Currently this just grabs the first watchdog device seen. */ if (shim->watchdog) { pr_info("%s: Ignoring extra watchdog device (%s)\n", DRV_NAME, wdd->info->identity); return -EEXIST; } BUG_ON(!wdd->ops->avm_hw_ping); if (!try_module_get(wdd->ops->owner)) { pr_err("%s: Can't get watchdog module %s\n", DRV_NAME, wdd->ops->owner->name); return -ENODEV; } wmb(); shim->watchdog = wdd; pr_info("%s: Device attached (%s)\n", DRV_NAME, wdd->info->identity); return 0; } static int ar7wdt_hw_shim_start_watchdog(struct ar7wdt_shim_info *shim) { int ret; pr_info("%s: Starting watchdog\n", DRV_NAME); ret = watchdog_start(shim->watchdog); if (ret) { pr_err("%s: Starting watchdog failed: %d\n", DRV_NAME, ret); return ret; } watchdog_nmi_enable(); return 0; } static int ar7wdt_hw_shim_stop_watchdog(struct ar7wdt_shim_info *shim) { int ret; if (!shim->watchdog) return 0; pr_info("%s: Stopping watchdog\n", DRV_NAME); ret = watchdog_stop(shim->watchdog); if (ret) { pr_err("%s: Error stopping watchdog: %d\n", DRV_NAME, ret); return ret; } watchdog_nmi_disable(); return 0; } static int ar7wdt_hw_shim_put_device(struct ar7wdt_shim_info *shim) { struct watchdog_device *wdd; wdd = shim->watchdog; if (!wdd) return 0; shim->watchdog = NULL; wmb(); module_put(wdd->ops->owner); return 0; } static int wdt_device_notify(struct notifier_block *nb, unsigned long action, void *data) { struct ar7wdt_shim_info *shim = container_of(nb, struct ar7wdt_shim_info, wdt_device_notifier); struct watchdog_device *wdd = data; int notified = NOTIFY_OK; int ret; mutex_lock(&shim->lock); switch (action) { case WATCHDOG_NOTIFY_ADD_DEVICE: ret = ar7wdt_hw_shim_get_device(shim, wdd); if (!ret) { if (shim->wdt_target == AR7WDT_TARGET_ACTIVE) ar7wdt_hw_shim_start_watchdog(shim); else ar7wdt_hw_shim_stop_watchdog(shim); } break; case WATCHDOG_NOTIFY_DEL_DEVICE: WARN(1, "%s: Unsolicited removal of watchdog device!\n", DRV_NAME); ar7wdt_hw_shim_stop_watchdog(shim); ar7wdt_hw_shim_put_device(shim); break; default: notified = NOTIFY_DONE; break; } mutex_unlock(&shim->lock); return notified; } static int __init ar7wdt_hw_shim_init(void) { struct ar7wdt_shim_info *shim = ar7wdt_hw_get_shim(); shim->wdt_device_notifier.notifier_call = wdt_device_notify; watchdog_register_notifier(&shim->wdt_device_notifier); return 0; } /***** ar7wdt interface functions *****/ void ar7wdt_hw_init(void) { struct ar7wdt_shim_info *shim = ar7wdt_hw_get_shim(); mutex_lock(&shim->lock); if (shim->wdt_target == AR7WDT_TARGET_DISABLED) goto out; shim->wdt_target = AR7WDT_TARGET_ACTIVE; if (shim->watchdog) ar7wdt_hw_shim_start_watchdog(shim); else pr_info("%s: %s: No watchdog device available (yet)\n", DRV_NAME, __func__); out: mutex_unlock(&shim->lock); register_nmi_notifier(&nmi_nb); } int ar7wdt_hw_is_wdt_running(void) { struct ar7wdt_shim_info *shim = ar7wdt_hw_get_shim(); bool active; mutex_lock(&shim->lock); if (shim->watchdog) active = watchdog_active(shim->watchdog); else active = false; mutex_unlock(&shim->lock); return active; } void ar7wdt_hw_secure_wdt_disable(void) { struct ar7wdt_shim_info *shim = ar7wdt_hw_get_shim(); mutex_lock(&shim->lock); ar7wdt_hw_shim_stop_watchdog(shim); shim->wdt_target = AR7WDT_TARGET_DISABLED; mutex_unlock(&shim->lock); } void ar7wdt_hw_deinit(void) { struct ar7wdt_shim_info *shim = ar7wdt_hw_get_shim(); mutex_lock(&shim->lock); ar7wdt_hw_shim_stop_watchdog(shim); ar7wdt_hw_shim_put_device(shim); mutex_unlock(&shim->lock); } void ar7wdt_hw_reboot(void) { panic("%s: soft reboot\n", __func__); } /* Handle LINUX_VERSION_CODE < KERNEL_VERSION(3,16,0) */ #ifndef smp_mb__before_atomic # define smp_mb__before_atomic smp_mb__before_atomic_inc #endif void ar7wdt_hw_trigger(void) { struct ar7wdt_shim_info *shim = ar7wdt_hw_get_shim(); struct watchdog_device *wdd = shim->watchdog; /* This must not sleep or take locks */ if (wdd && wdd->ops->avm_hw_ping) wdd->ops->avm_hw_ping(wdd); hit_watchdog_snooze_button(); } module_init(ar7wdt_hw_shim_init);