// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (c) 2020 Samsung Electronics Co., Ltd. * http://www.samsung.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and * only version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #define pr_fmt(fmt) "sysevent: %s(): " fmt, __func__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static uint disable_sysevent_support; module_param(disable_sysevent_support, uint, 0644); /** * enum p_sysevent_state - state of a sysevent (private) * @SYSTEM_EVENT_NORMAL: sysevent is operating normally * @SYSTEM_EVENT_CRASHED: sysevent has crashed and hasn't been shutdown * @SYSTEM_EVENT_RESTARTING: sysevent has been shutdown and is now restarting * * The 'private' side of the subsytem state used to determine where in the * restart process the sysevent is. */ enum p_sysevent_state { SYSTEM_EVENT_NORMAL, SYSTEM_EVENT_CRASHED, SYSTEM_EVENT_RESTARTING, }; /** * enum sysevent_state - state of a sysevent (public) * @SYSTEM_EVENT_OFFLINING: sysevent is offlining * @SYSTEM_EVENT_OFFLINE: sysevent is offline * @sysevent_ONLINE: sysevent is online * * The 'public' side of the subsytem state, exposed to userspace. */ enum sysevent_state { SYSTEM_EVENT_OFFLINING, SYSTEM_EVENT_OFFLINE, SYSTEM_EVENT_ONLINE, }; static const char * const sysevent_states[] = { [SYSTEM_EVENT_OFFLINING] = "OFFLINING", [SYSTEM_EVENT_OFFLINE] = "OFFLINE", [SYSTEM_EVENT_ONLINE] = "ONLINE", }; static const char * const restart_levels[] = { [RESET_SOC] = "SYSTEM", [RESET_SYSTEM_EVENT_COUPLED] = "RELATED", }; /** * struct sysevent_tracking - track state of a sysevent or restart order * @p_state: private state of sysevent/order * @state: public state of sysevent/order * @s_lock: protects p_state * @lock: protects sysevent/order callbacks and state * * Tracks the state of a sysevent or a set of sysevents (restart order). * Doing this avoids the need to grab each sysevent's lock and update * each sysevents state when restarting an order. */ struct sysevent_tracking { enum p_sysevent_state p_state; spinlock_t s_lock; enum sysevent_state state; struct mutex lock; }; struct restart_log { struct timespec64 time; struct sysevent_device *dev; struct list_head list; }; /** * struct sysevent_device - sysevent device * @desc: sysevent descriptor * @work: context for sysevent_restart_wq_func() for this device * @sysevent_wlock: prevents suspend during sysevent_restart() * @device_restart_work: work struct for device restart * @track: state tracking and locking * @notify: sysevent notify handle * @dev: device * @owner: module that provides @desc * @count: reference count of sysevent_get()/sysevent_put() * @id: ida * @restart_level: restart level (0 - panic, 1 - related, 2 - independent, etc.) * @restart_order: order of other devices this devices restarts with * @crash_count: number of times the device has crashed * @do_ramdump_on_put: ramdump on sysevent_put() if true * @err_ready: completion variable to record error ready from sysevent * @crashed: indicates if sysevent has crashed * @notif_state: current state of sysevent in terms of sysevent notifications */ struct sysevent_device { struct sysevent_desc *desc; struct work_struct work; struct wakeup_source *sysevent_wlock; char wlname[64]; struct work_struct device_restart_work; struct sysevent_tracking track; void *notify; void *early_notify; struct device dev; struct module *owner; int count; int id; int restart_level; int crash_count; bool do_ramdump_on_put; struct cdev char_dev; dev_t dev_no; enum crash_status crashed; int notif_state; struct list_head list; }; static struct sysevent_device *to_sysevent(struct device *d) { return container_of(d, struct sysevent_device, dev); } static struct sysevent_tracking *sysevent_get_track(struct sysevent_device *sysevent) { return &sysevent->track; } static ssize_t name_show(struct device *dev, struct device_attribute *attr, char *buf) { return snprintf(buf, PAGE_SIZE, "%s\n", to_sysevent(dev)->desc->name); } static DEVICE_ATTR_RO(name); static ssize_t state_show(struct device *dev, struct device_attribute *attr, char *buf) { enum sysevent_state state = to_sysevent(dev)->track.state; return snprintf(buf, PAGE_SIZE, "%s\n", sysevent_states[state]); } static DEVICE_ATTR_RO(state); static ssize_t crash_count_show(struct device *dev, struct device_attribute *attr, char *buf) { return snprintf(buf, PAGE_SIZE, "%d\n", to_sysevent(dev)->crash_count); } static DEVICE_ATTR_RO(crash_count); static ssize_t restart_level_show(struct device *dev, struct device_attribute *attr, char *buf) { int level = to_sysevent(dev)->restart_level; return snprintf(buf, PAGE_SIZE, "%s\n", restart_levels[level]); } static ssize_t crash_reason_show(struct device *dev, struct device_attribute *attr, char *buf) { int ret; unsigned long flags; struct sysevent_device *sysevent = to_sysevent(dev); spin_lock_irqsave(&sysevent->desc->sysevent_sysfs_lock, flags); ret = snprintf(buf, PAGE_SIZE, "%s\n", to_sysevent(dev)->desc->last_crash_reason); spin_unlock_irqrestore(&sysevent->desc->sysevent_sysfs_lock, flags); return ret; } static DEVICE_ATTR_RO(crash_reason); static ssize_t crash_timestamp_show(struct device *dev, struct device_attribute *attr, char *buf) { int ret; unsigned long flags; struct sysevent_device *sysevent = to_sysevent(dev); spin_lock_irqsave(&sysevent->desc->sysevent_sysfs_lock, flags); ret = snprintf(buf, PAGE_SIZE, "%s\n", to_sysevent(dev)->desc->last_crash_timestamp); spin_unlock_irqrestore(&sysevent->desc->sysevent_sysfs_lock, flags); return ret; } static DEVICE_ATTR_RO(crash_timestamp); static ssize_t restart_level_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct sysevent_device *sysevent = to_sysevent(dev); const char *p; int i, orig_count = count; p = memchr(buf, '\n', count); if (p) count = p - buf; for (i = 0; i < ARRAY_SIZE(restart_levels); i++) if (!strncasecmp(buf, restart_levels[i], count)) { sysevent->restart_level = i; return orig_count; } return -EPERM; } static DEVICE_ATTR_RW(restart_level); static ssize_t firmware_name_show(struct device *dev, struct device_attribute *attr, char *buf) { return snprintf(buf, PAGE_SIZE, "%s\n", to_sysevent(dev)->desc->fw_name); } static ssize_t firmware_name_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct sysevent_device *sysevent = to_sysevent(dev); struct sysevent_tracking *track = sysevent_get_track(sysevent); const char *p; int orig_count = count; p = memchr(buf, '\n', count); if (p) count = p - buf; pr_info("Changing sysevent fw_name to %s\n", buf); mutex_lock(&track->lock); strlcpy(sysevent->desc->fw_name, buf, min(count + 1, sizeof(sysevent->desc->fw_name))); mutex_unlock(&track->lock); return orig_count; } static DEVICE_ATTR_RW(firmware_name); int sysevent_get_restart_level(struct sysevent_device *dev) { return dev->restart_level; } EXPORT_SYMBOL(sysevent_get_restart_level); static void sysevent_set_state(struct sysevent_device *sysevent, enum sysevent_state state) { unsigned long flags; spin_lock_irqsave(&sysevent->track.s_lock, flags); if (sysevent->track.state != state) { sysevent->track.state = state; spin_unlock_irqrestore(&sysevent->track.s_lock, flags); sysfs_notify(&sysevent->dev.kobj, NULL, "state"); return; } spin_unlock_irqrestore(&sysevent->track.s_lock, flags); } /** * subsytem_default_online() - Mark a sysevent as online by default * @dev: sysevent to mark as online * * Marks a sysevent as "online" without increasing the reference count * on the sysevent. This is typically used by sysevents that are already * online when the kernel boots up. */ void sysevent_default_online(struct sysevent_device *dev) { sysevent_set_state(dev, SYSTEM_EVENT_ONLINE); } EXPORT_SYMBOL(sysevent_default_online); static struct attribute *sysevent_attrs[] = { &dev_attr_name.attr, &dev_attr_state.attr, &dev_attr_crash_count.attr, &dev_attr_crash_reason.attr, &dev_attr_crash_timestamp.attr, &dev_attr_restart_level.attr, &dev_attr_firmware_name.attr, NULL, }; ATTRIBUTE_GROUPS(sysevent); struct bus_type sysevent_bus_type = { .name = "exynos_sysevent", .dev_groups = sysevent_groups, }; EXPORT_SYMBOL(sysevent_bus_type); static DEFINE_IDA(sysevent_ida); static int enable_ramdumps; module_param(enable_ramdumps, int, 0644); struct workqueue_struct *sysevent_wq; static struct class *char_class; static LIST_HEAD(restart_log_list); static LIST_HEAD(sysevent_list); static DEFINE_MUTEX(restart_log_mutex); static DEFINE_MUTEX(sysevent_list_lock); static DEFINE_MUTEX(char_device_lock); static int max_restarts; module_param(max_restarts, int, 0644); static long max_history_time = 3600; module_param(max_history_time, long, 0644); static void do_epoch_check(struct sysevent_device *dev) { int n = 0; struct timespec64 *time_first = NULL, *curr_time; struct timespec64 now; struct restart_log *r_log, *temp; static int max_restarts_check; static long max_history_time_check; mutex_lock(&restart_log_mutex); max_restarts_check = max_restarts; max_history_time_check = max_history_time; /* Check if epoch checking is enabled */ if (!max_restarts_check) goto out; r_log = kmalloc(sizeof(struct restart_log), GFP_KERNEL); if (!r_log) goto out; r_log->dev = dev; ktime_get_real_ts64(&now); r_log->time = now; curr_time = &r_log->time; INIT_LIST_HEAD(&r_log->list); list_add_tail(&r_log->list, &restart_log_list); list_for_each_entry_safe(r_log, temp, &restart_log_list, list) { if ((curr_time->tv_sec - r_log->time.tv_sec) > max_history_time_check) { pr_debug("Deleted node with restart_time = %ld\n", r_log->time.tv_sec); list_del(&r_log->list); kfree(r_log); continue; } if (!n) { time_first = &r_log->time; pr_debug("Time_first: %ld\n", time_first->tv_sec); } n++; pr_debug("Restart_time: %ld\n", r_log->time.tv_sec); } if (time_first && n >= max_restarts_check) { if ((curr_time->tv_sec - time_first->tv_sec) < max_history_time_check) panic("sysevents have crashed %d times in less than %ld seconds!", max_restarts_check, max_history_time_check); } out: mutex_unlock(&restart_log_mutex); } static int is_ramdump_enabled(struct sysevent_device *dev) { return enable_ramdumps; } static int for_each_sysevent_device(struct sysevent_device **list, unsigned int count, void *data, int (*fn)(struct sysevent_device *, void *)) { int ret; while (count--) { struct sysevent_device *dev = *list++; if (!dev) continue; ret = fn(dev, data); if (ret) return ret; } return 0; } static void sysevent_notif_uevent(struct sysevent_desc *desc, enum sysevent_notif_type notif) { char *envp[3]; if (notif == SYSTEM_EVENT_AFTER_POWERUP) { envp[0] = kasprintf(GFP_KERNEL, "SYSTEM_EVENT=%s", desc->name); envp[1] = kasprintf(GFP_KERNEL, "NOTIFICATION=%d", notif); envp[2] = NULL; kobject_uevent_env(&desc->dev->kobj, KOBJ_CHANGE, envp); pr_debug("%s %s sent\n", envp[0], envp[1]); kfree(envp[1]); kfree(envp[0]); } } static void notify_each_sysevent_device(struct sysevent_device **list, unsigned int count, enum sysevent_notif_type notif, void *data) { while (count--) { struct sysevent_device *dev = *list++; struct notif_data notif_data; struct platform_device *pdev; if (!dev) continue; pdev = container_of(dev->desc->dev, struct platform_device, dev); dev->notif_state = notif; notif_data.crashed = sysevent_get_crash_status(dev); notif_data.enable_ramdump = is_ramdump_enabled(dev); notif_data.no_auth = dev->desc->no_auth; notif_data.pdev = pdev; sysevent_notif_queue_notification(dev->notify, notif, ¬if_data); if (0) sysevent_notif_uevent(dev->desc, notif); } } static void enable_all_irqs(struct sysevent_device *dev) { if (dev->desc->watchdog_irq && dev->desc->watchdog_handler) { enable_irq(dev->desc->watchdog_irq); irq_set_irq_wake(dev->desc->watchdog_irq, 1); } if (dev->desc->err_fatal_irq && dev->desc->err_fatal_handler) enable_irq(dev->desc->err_fatal_irq); if (dev->desc->generic_irq && dev->desc->generic_handler) { enable_irq(dev->desc->generic_irq); irq_set_irq_wake(dev->desc->generic_irq, 1); } } static void disable_all_irqs(struct sysevent_device *dev) { if (dev->desc->watchdog_irq && dev->desc->watchdog_handler) { disable_irq(dev->desc->watchdog_irq); irq_set_irq_wake(dev->desc->watchdog_irq, 0); } if (dev->desc->err_fatal_irq && dev->desc->err_fatal_handler) disable_irq(dev->desc->err_fatal_irq); if (dev->desc->generic_irq && dev->desc->generic_handler) { disable_irq(dev->desc->generic_irq); irq_set_irq_wake(dev->desc->generic_irq, 0); } } static int sysevent_shutdown(struct sysevent_device *dev, void *data) { const char *name = dev->desc->name; char *timestamp = dev->desc->last_crash_timestamp; int ret; struct timespec64 ts_rtc; struct rtc_time tm; unsigned long flags; pr_info("[%s:%d]: Shutting down %s\n", current->comm, current->pid, name); ret = dev->desc->shutdown(dev->desc, true); if (ret < 0) { if (!dev->desc->ignore_sysevent_failure) { panic("sysevent-restart: [%s:%d]: Failed to shutdown %s!", current->comm, current->pid, name); } else { pr_err("Shutdown failure on %s\n", name); return ret; } } spin_lock_irqsave(&dev->desc->sysevent_sysfs_lock, flags); /* record crash time */ ktime_get_real_ts64(&ts_rtc); rtc_time64_to_tm(ts_rtc.tv_sec - (sys_tz.tz_minuteswest * 60), &tm); snprintf(timestamp, MAX_CRASH_TIMESTAMP_LEN, "%d-%02d-%02d_%02d-%02d-%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); spin_unlock_irqrestore(&dev->desc->sysevent_sysfs_lock, flags); dev->crash_count++; sysevent_set_state(dev, SYSTEM_EVENT_OFFLINE); disable_all_irqs(dev); return 0; } static int sysevent_ramdump(struct sysevent_device *dev, void *data) { const char *name = dev->desc->name; if (dev->desc->ramdump) if (dev->desc->ramdump(is_ramdump_enabled(dev), dev->desc) < 0) pr_warn("%s[%s:%d]: Ramdump failed.\n", name, current->comm, current->pid); dev->do_ramdump_on_put = false; return 0; } static int sysevent_free_memory(struct sysevent_device *dev, void *data) { if (dev->desc->free_memory) dev->desc->free_memory(dev->desc); return 0; } static int sysevent_powerup(struct sysevent_device *dev, void *data) { const char *name = dev->desc->name; int ret; pr_info("[%s:%d]: Powering up %s\n", current->comm, current->pid, name); ret = dev->desc->powerup(dev->desc); if (ret < 0) { notify_each_sysevent_device(&dev, 1, SYSTEM_EVENT_POWERUP_FAILURE, NULL); if (system_state == SYSTEM_RESTART || system_state == SYSTEM_POWER_OFF) WARN(1, "sysevent aborted: %s, system reboot/shutdown is under way\n", name); else { if (!dev->desc->ignore_sysevent_failure) { /* * There is a slight window between reboot and * system_state changing to SYSTEM_RESTART or * SYSTEM_POWER_OFF. Add a delay before panic * to ensure sysevent that happens during reboot * will not result in a kernel panic. */ msleep(3000); if (system_state != SYSTEM_RESTART && system_state != SYSTEM_POWER_OFF) panic("[%s:%d]: Powerup error: %s!", current->comm, current->pid, name); } pr_err("Powerup failure on %s\n", name); } return ret; } enable_all_irqs(dev); sysevent_set_state(dev, SYSTEM_EVENT_ONLINE); sysevent_set_crash_status(dev, CRASH_STATUS_NO_CRASH); return 0; } static int __find_sysevent_device(struct device *dev, const void *data) { struct sysevent_device *sysevent = to_sysevent(dev); return !strcmp(sysevent->desc->name, data); } struct sysevent_device *find_sysevent_device(const char *str) { struct device *dev; if (!str) return NULL; dev = bus_find_device(&sysevent_bus_type, NULL, (void *)str, __find_sysevent_device); return dev ? to_sysevent(dev) : NULL; } EXPORT_SYMBOL(find_sysevent_device); static int sysevent_start(struct sysevent_device *sysevent) { int ret; notify_each_sysevent_device(&sysevent, 1, SYSTEM_EVENT_BEFORE_POWERUP, NULL); ret = sysevent->desc->powerup(sysevent->desc); if (ret) { notify_each_sysevent_device(&sysevent, 1, SYSTEM_EVENT_POWERUP_FAILURE, NULL); return ret; } enable_all_irqs(sysevent); sysevent_set_state(sysevent, SYSTEM_EVENT_ONLINE); sysevent_set_crash_status(sysevent, CRASH_STATUS_NO_CRASH); notify_each_sysevent_device(&sysevent, 1, SYSTEM_EVENT_AFTER_POWERUP, NULL); return ret; } static void sysevent_stop(struct sysevent_device *sysevent) { notify_each_sysevent_device(&sysevent, 1, SYSTEM_EVENT_BEFORE_SHUTDOWN, NULL); sysevent->desc->shutdown(sysevent->desc, false); sysevent_set_state(sysevent, SYSTEM_EVENT_OFFLINE); disable_all_irqs(sysevent); notify_each_sysevent_device(&sysevent, 1, SYSTEM_EVENT_AFTER_SHUTDOWN, NULL); } int sysevent_set_fwname(const char *name, const char *fw_name) { struct sysevent_device *sysevent; if (!name) return -EINVAL; if (!fw_name) return -EINVAL; sysevent = find_sysevent_device(name); if (!sysevent) return -EINVAL; pr_debug("Changing sysevent [%s] fw_name to [%s]\n", name, fw_name); strlcpy(sysevent->desc->fw_name, fw_name, sizeof(sysevent->desc->fw_name)); return 0; } EXPORT_SYMBOL(sysevent_set_fwname); void *__sysevent_get(const char *name, const char *fw_name) { struct sysevent_device *sysevent; int ret; void *retval; struct sysevent_tracking *track; if (!name) return NULL; sysevent = retval = find_sysevent_device(name); if (!sysevent) return ERR_PTR(-ENODEV); if (!try_module_get(sysevent->owner)) { retval = ERR_PTR(-ENODEV); goto err_module; } track = sysevent_get_track(sysevent); mutex_lock(&track->lock); if (!sysevent->count) { if (fw_name) { pr_info("Changing sysevent fw_name to %s\n", fw_name); strlcpy(sysevent->desc->fw_name, fw_name, sizeof(sysevent->desc->fw_name)); } ret = sysevent_start(sysevent); if (ret) { retval = ERR_PTR(ret); goto err_start; } } sysevent->count++; mutex_unlock(&track->lock); return retval; err_start: mutex_unlock(&track->lock); module_put(sysevent->owner); err_module: put_device(&sysevent->dev); return retval; } /** * subsytem_get() - Boot a sysevent * @name: pointer to a string containing the name of the sysevent to boot * * This function returns a pointer if it succeeds. If an error occurs an * ERR_PTR is returned. * * If this feature is disable, the value %NULL will be returned. */ void *sysevent_get(const char *name) { return __sysevent_get(name, NULL); } EXPORT_SYMBOL(sysevent_get); /** * sysevent_get_with_fwname() - Boot a sysevent using the firmware name passed * @name: pointer to a string containing the name of the sysevent to boot * @fw_name: pointer to a string containing the sysevent firmware image name * * This function returns a pointer if it succeeds. If an error occurs an * ERR_PTR is returned. * * If this feature is disable, the value %NULL will be returned. */ void *sysevent_get_with_fwname(const char *name, const char *fw_name) { return __sysevent_get(name, fw_name); } EXPORT_SYMBOL(sysevent_get_with_fwname); /** * sysevent_put() - Shutdown a sysevent * @peripheral_handle: pointer from a previous call to sysevent_get() * * This doesn't imply that a sysevent is shutdown until all callers of * sysevent_get() have called sysevent_put(). */ void sysevent_put(void *sys) { struct sysevent_device *sysevent = sys; struct sysevent_tracking *track; if (IS_ERR_OR_NULL(sysevent)) return; track = sysevent_get_track(sysevent); mutex_lock(&track->lock); if (WARN(!sysevent->count, "%s: %s: Reference count mismatch\n", sysevent->desc->name, __func__)) goto err_out; if (!--sysevent->count) { sysevent_stop(sysevent); if (sysevent->do_ramdump_on_put) sysevent_ramdump(sysevent, NULL); sysevent_free_memory(sysevent, NULL); } mutex_unlock(&track->lock); module_put(sysevent->owner); put_device(&sysevent->dev); return; err_out: mutex_unlock(&track->lock); } EXPORT_SYMBOL(sysevent_put); static void sysevent_restart_wq_func(struct work_struct *work) { struct sysevent_device *dev = container_of(work, struct sysevent_device, work); struct sysevent_device **list; struct sysevent_desc *desc = dev->desc; struct sysevent_tracking *track; unsigned int count; unsigned long flags; int ret; /* * It's OK to not take the registration lock at this point. * This is because the sysevent list inside the relevant * restart order is not being traversed. */ list = &dev; count = 1; track = &dev->track; /* * If a system reboot/shutdown is under way, ignore sysevent errors. * However, print a message so that we know that a sysevent behaved * unexpectedly here. */ if (system_state == SYSTEM_RESTART || system_state == SYSTEM_POWER_OFF) { WARN(1, "sysevent aborted: %s, system reboot/shutdown is under way\n", desc->name); return; } mutex_lock(&track->lock); do_epoch_check(dev); if (dev->track.state == SYSTEM_EVENT_OFFLINE) { mutex_unlock(&track->lock); WARN(1, "sysevent aborted: %s sysevent not online\n", desc->name); return; } /* * It's necessary to take the registration lock because the sysevent * list in the SoC restart order will be traversed and it shouldn't be * changed until _this_ restart sequence completes. */ pr_debug("[%s:%d]: Starting restart sequence for %s\n", current->comm, current->pid, desc->name); notify_each_sysevent_device(list, count, SYSTEM_EVENT_BEFORE_SHUTDOWN, NULL); ret = for_each_sysevent_device(list, count, NULL, sysevent_shutdown); if (ret) goto err; notify_each_sysevent_device(list, count, SYSTEM_EVENT_AFTER_SHUTDOWN, NULL); notify_each_sysevent_device(list, count, SYSTEM_EVENT_RAMDUMP_NOTIFICATION, NULL); spin_lock_irqsave(&track->s_lock, flags); track->p_state = SYSTEM_EVENT_RESTARTING; spin_unlock_irqrestore(&track->s_lock, flags); /* Collect ram dumps for all sysevents in order here */ for_each_sysevent_device(list, count, NULL, sysevent_ramdump); for_each_sysevent_device(list, count, NULL, sysevent_free_memory); notify_each_sysevent_device(list, count, SYSTEM_EVENT_BEFORE_POWERUP, NULL); ret = for_each_sysevent_device(list, count, NULL, sysevent_powerup); if (ret) goto err; notify_each_sysevent_device(list, count, SYSTEM_EVENT_AFTER_POWERUP, NULL); pr_info("[%s:%d]: Restart sequence for %s completed.\n", current->comm, current->pid, desc->name); err: /* Reset sysevent count */ if (ret) dev->count = 0; mutex_unlock(&track->lock); spin_lock_irqsave(&track->s_lock, flags); track->p_state = SYSTEM_EVENT_NORMAL; __pm_relax(dev->sysevent_wlock); spin_unlock_irqrestore(&track->s_lock, flags); } static void __sysevent_restart_dev(struct sysevent_device *dev) { struct sysevent_desc *desc = dev->desc; const char *name = dev->desc->name; struct sysevent_tracking *track; unsigned long flags; pr_debug("Restarting %s [level=%s]!\n", desc->name, restart_levels[dev->restart_level]); track = sysevent_get_track(dev); /* * Allow drivers to call sysevent_restart{_dev}() as many times as * they want up until the point where the sysevent is shutdown. */ spin_lock_irqsave(&track->s_lock, flags); if (track->p_state != SYSTEM_EVENT_CRASHED && dev->track.state == SYSTEM_EVENT_ONLINE) { if (track->p_state != SYSTEM_EVENT_RESTARTING) { track->p_state = SYSTEM_EVENT_CRASHED; __pm_stay_awake(dev->sysevent_wlock); queue_work(sysevent_wq, &dev->work); } else { panic("sysevent %s crashed during sysevent!", name); } } else WARN(dev->track.state == SYSTEM_EVENT_OFFLINE, "sysevent aborted: %s sysevent not online\n", name); spin_unlock_irqrestore(&track->s_lock, flags); } static void device_restart_work_hdlr(struct work_struct *work) { struct sysevent_device *dev = container_of(work, struct sysevent_device, device_restart_work); notify_each_sysevent_device(&dev, 1, SYSTEM_EVENT_SOC_RESET, NULL); /* * Temporary workaround until ramdump userspace application calls * sync() and fclose() on atpting the dump. */ msleep(100); panic("sysevent-restart: Resetting the SoC - %s crashed.", dev->desc->name); } int sysevent_restart_dev(struct sysevent_device *dev) { const char *name; if (!get_device(&dev->dev)) return -ENODEV; if (!try_module_get(dev->owner)) { put_device(&dev->dev); return -ENODEV; } name = dev->desc->name; send_early_notifications(dev->early_notify); /* * If a system reboot/shutdown is underway, ignore sysevent errors. * However, print a message so that we know that a sysevent behaved * unexpectedly here. */ if (system_state == SYSTEM_RESTART || system_state == SYSTEM_POWER_OFF) { pr_err("%s crashed during a system poweroff/shutdown.\n", name); return -EBUSY; } pr_info("Restart sequence requested for %s, restart_level = %s.\n", name, restart_levels[dev->restart_level]); /* TODO - If sysevent is not support, This function must be needed */ if (disable_sysevent_support == true) { pr_warn("sysevent-restart: Ignoring restart request for %s\n", name); return 0; } /* TODO - modify for exynos */ switch (dev->restart_level) { case RESET_SYSTEM_EVENT_COUPLED: __sysevent_restart_dev(dev); break; case RESET_SOC: __pm_stay_awake(dev->sysevent_wlock); schedule_work(&dev->device_restart_work); return 0; default: panic("sysevent-restart: Unknown restart level!\n"); break; } /* Till here */ module_put(dev->owner); put_device(&dev->dev); return 0; } EXPORT_SYMBOL(sysevent_restart_dev); int sysevent_restart(const char *name) { int ret; struct sysevent_device *dev = find_sysevent_device(name); if (!dev) return -ENODEV; ret = sysevent_restart_dev(dev); put_device(&dev->dev); return ret; } EXPORT_SYMBOL(sysevent_restart); int sysevent_crashed(const char *name) { struct sysevent_device *dev = find_sysevent_device(name); struct sysevent_tracking *track; if (!dev) return -ENODEV; if (!get_device(&dev->dev)) return -ENODEV; track = sysevent_get_track(dev); mutex_lock(&track->lock); dev->do_ramdump_on_put = true; /* * TODO: Make this work with multiple consumers where one is calling * sysevent_restart() and another is calling this function. To do * so would require updating private state, etc. */ mutex_unlock(&track->lock); put_device(&dev->dev); return 0; } EXPORT_SYMBOL(sysevent_crashed); void sysevent_set_crash_status(struct sysevent_device *dev, enum crash_status crashed) { dev->crashed = crashed; } EXPORT_SYMBOL(sysevent_set_crash_status); enum crash_status sysevent_get_crash_status(struct sysevent_device *dev) { return dev->crashed; } static int sysevent_device_open(struct inode *inode, struct file *file) { struct sysevent_device *device, *sysevent_dev = 0; void *retval; mutex_lock(&sysevent_list_lock); list_for_each_entry(device, &sysevent_list, list) if (MINOR(device->dev_no) == iminor(inode)) sysevent_dev = device; mutex_unlock(&sysevent_list_lock); if (!sysevent_dev) return -EINVAL; retval = sysevent_get_with_fwname(sysevent_dev->desc->name, sysevent_dev->desc->fw_name); if (IS_ERR(retval)) return PTR_ERR(retval); return 0; } static int sysevent_device_close(struct inode *inode, struct file *file) { struct sysevent_device *device, *sysevent_dev = 0; mutex_lock(&sysevent_list_lock); list_for_each_entry(device, &sysevent_list, list) if (MINOR(device->dev_no) == iminor(inode)) sysevent_dev = device; mutex_unlock(&sysevent_list_lock); if (!sysevent_dev) return -EINVAL; sysevent_put(sysevent_dev); return 0; } static const struct file_operations sysevent_device_fops = { .owner = THIS_MODULE, .open = sysevent_device_open, .release = sysevent_device_close, }; static void sysevent_device_release(struct device *dev) { struct sysevent_device *sysevent = to_sysevent(dev); wakeup_source_unregister(sysevent->sysevent_wlock); mutex_destroy(&sysevent->track.lock); ida_simple_remove(&sysevent_ida, sysevent->id); kfree(sysevent); } static int sysevent_char_device_add(struct sysevent_device *sysevent_dev) { int ret = 0; static int major, minor; dev_t dev_no; mutex_lock(&char_device_lock); if (!major) { ret = alloc_chrdev_region(&dev_no, 0, 4, "sysevent"); if (ret < 0) { pr_err("Failed to alloc sysevent_dev region, err %d\n", ret); goto fail; } major = MAJOR(dev_no); minor = MINOR(dev_no); } else dev_no = MKDEV(major, minor); if (!device_create(char_class, sysevent_dev->desc->dev, dev_no, NULL, "sysevent_%s", sysevent_dev->desc->name)) { pr_err("Failed to create sysevent_%s device\n", sysevent_dev->desc->name); goto fail_unregister_cdev_region; } cdev_init(&sysevent_dev->char_dev, &sysevent_device_fops); sysevent_dev->char_dev.owner = THIS_MODULE; ret = cdev_add(&sysevent_dev->char_dev, dev_no, 1); if (ret < 0) goto fail_destroy_device; sysevent_dev->dev_no = dev_no; minor++; mutex_unlock(&char_device_lock); return 0; fail_destroy_device: device_destroy(char_class, dev_no); fail_unregister_cdev_region: unregister_chrdev_region(dev_no, 1); fail: mutex_unlock(&char_device_lock); return ret; } static void sysevent_char_device_remove(struct sysevent_device *sysevent_dev) { cdev_del(&sysevent_dev->char_dev); device_destroy(char_class, sysevent_dev->dev_no); unregister_chrdev_region(sysevent_dev->dev_no, 1); } static int __get_irq(struct sysevent_desc *desc, const char *prop, unsigned int *irq) { int irql = 0; struct device_node *dnode = desc->dev->of_node; if (of_property_match_string(dnode, "interrupt-names", prop) < 0) return -ENOENT; irql = of_irq_get_byname(dnode, prop); if (irql < 0) { pr_err("[%s]: Error getting IRQ \"%s\"\n", desc->name, prop); return irql; } *irq = irql; return 0; } static int sysevent_parse_devicetree(struct sysevent_desc *desc) { int ret; struct platform_device *pdev = container_of(desc->dev, struct platform_device, dev); ret = __get_irq(desc, "samsung,err-fatal", &desc->err_fatal_irq); if (ret && ret != -ENOENT) return ret; ret = __get_irq(desc, "samsung,watchdog", &desc->watchdog_irq); if (ret && ret != -ENOENT) return ret; if (of_property_read_bool(pdev->dev.of_node, "samsung,sysevent-generic-irq-handler")) { ret = platform_get_irq(pdev, 0); if (ret > 0) desc->generic_irq = ret; } desc->ignore_sysevent_failure = of_property_read_bool(pdev->dev.of_node, "samsung,ignore-sysevent-failure"); return 0; } static int sysevent_setup_irqs(struct sysevent_device *sysevent) { struct sysevent_desc *desc = sysevent->desc; int ret; if (desc->err_fatal_irq && desc->err_fatal_handler) { ret = devm_request_threaded_irq(desc->dev, desc->err_fatal_irq, NULL, desc->err_fatal_handler, IRQF_TRIGGER_RISING, desc->name, desc); if (ret < 0) { dev_err(desc->dev, "[%s]: Unable to register error fatal IRQ handler: %d, irq is %d\n", desc->name, ret, desc->err_fatal_irq); return ret; } disable_irq(desc->err_fatal_irq); } if (desc->watchdog_irq && desc->watchdog_handler) { ret = devm_request_irq(desc->dev, desc->watchdog_irq, desc->watchdog_handler, IRQF_TRIGGER_RISING, desc->name, desc); if (ret < 0) { dev_err(desc->dev, "[%s]: Unable to register watchdog bite handler: %d\n", desc->name, ret); return ret; } disable_irq(desc->watchdog_irq); } if (desc->generic_irq && desc->generic_handler) { ret = devm_request_irq(desc->dev, desc->generic_irq, desc->generic_handler, IRQF_TRIGGER_HIGH, desc->name, desc); if (ret < 0) { dev_err(desc->dev, "[%s]: Unable to register generic irq handler: %d\n", desc->name, ret); return ret; } disable_irq(desc->generic_irq); } return 0; } static void sysevent_free_irqs(struct sysevent_device *sysevent) { struct sysevent_desc *desc = sysevent->desc; if (desc->err_fatal_irq && desc->err_fatal_handler) devm_free_irq(desc->dev, desc->err_fatal_irq, desc); if (desc->watchdog_irq && desc->watchdog_handler) devm_free_irq(desc->dev, desc->watchdog_irq, desc); } struct sysevent_device *sysevent_register(struct sysevent_desc *desc) { struct sysevent_device *sysevent; struct device_node *ofnode = desc->dev->of_node; int ret; sysevent = kzalloc(sizeof(*sysevent), GFP_KERNEL); if (!sysevent) return ERR_PTR(-ENOMEM); sysevent->desc = desc; sysevent->owner = desc->owner; sysevent->dev.parent = desc->dev; sysevent->dev.bus = &sysevent_bus_type; sysevent->dev.release = sysevent_device_release; sysevent->notif_state = -1; strlcpy(sysevent->desc->fw_name, desc->name, sizeof(sysevent->desc->fw_name)); sysevent->notify = sysevent_notif_add_sysevent(desc->name); sysevent->early_notify = sysevent_get_early_notif_info(desc->name); snprintf(sysevent->wlname, sizeof(sysevent->wlname), "sysevent(%s)", desc->name); sysevent->sysevent_wlock = wakeup_source_register(&sysevent->dev, sysevent->wlname); INIT_WORK(&sysevent->work, sysevent_restart_wq_func); INIT_WORK(&sysevent->device_restart_work, device_restart_work_hdlr); spin_lock_init(&sysevent->track.s_lock); spin_lock_init(&sysevent->desc->sysevent_sysfs_lock); sysevent->id = ida_simple_get(&sysevent_ida, 0, 0, GFP_KERNEL); if (sysevent->id < 0) { ret = sysevent->id; kfree(sysevent); return ERR_PTR(ret); } dev_set_name(&sysevent->dev, "sysevent%d", sysevent->id); mutex_init(&sysevent->track.lock); ret = device_register(&sysevent->dev); if (ret) { put_device(&sysevent->dev); return ERR_PTR(ret); } ret = sysevent_char_device_add(sysevent); if (ret) goto err_register; if (ofnode) { ret = sysevent_parse_devicetree(desc); if (ret) goto err_register; } mutex_lock(&sysevent_list_lock); INIT_LIST_HEAD(&sysevent->list); list_add_tail(&sysevent->list, &sysevent_list); mutex_unlock(&sysevent_list_lock); if (ofnode) { ret = sysevent_setup_irqs(sysevent); if (ret < 0) goto err_setup_irqs; } return sysevent; err_setup_irqs: err_register: device_unregister(&sysevent->dev); return ERR_PTR(ret); } EXPORT_SYMBOL(sysevent_register); void sysevent_unregister(struct sysevent_device *sysevent) { struct sysevent_device *sysevent_dev, *tmp; struct device_node *device = sysevent->desc->dev->of_node; if (IS_ERR_OR_NULL(sysevent)) return; if (get_device(&sysevent->dev)) { mutex_lock(&sysevent_list_lock); list_for_each_entry_safe(sysevent_dev, tmp, &sysevent_list, list) if (sysevent_dev == sysevent) list_del(&sysevent->list); mutex_unlock(&sysevent_list_lock); if (device) sysevent_free_irqs(sysevent); mutex_lock(&sysevent->track.lock); WARN_ON(sysevent->count); device_unregister(&sysevent->dev); mutex_unlock(&sysevent->track.lock); sysevent_char_device_remove(sysevent); put_device(&sysevent->dev); } } EXPORT_SYMBOL(sysevent_unregister); static int sysevent_panic(struct device *dev, void *data) { struct sysevent_device *sysevent = to_sysevent(dev); if (sysevent->desc->crash_shutdown) sysevent->desc->crash_shutdown(sysevent->desc); return 0; } static int sysevent_panic_handler(struct notifier_block *this, unsigned long event, void *ptr) { bus_for_each_dev(&sysevent_bus_type, NULL, NULL, sysevent_panic); return NOTIFY_DONE; } static struct notifier_block panic_nb = { .notifier_call = sysevent_panic_handler, }; static int __init sysevent_init(void) { int ret; sysevent_wq = alloc_workqueue("sysevent_wq", WQ_UNBOUND | WQ_HIGHPRI | WQ_CPU_INTENSIVE, 0); BUG_ON(!sysevent_wq); ret = bus_register(&sysevent_bus_type); if (ret) goto err_bus; char_class = class_create(THIS_MODULE, "sysevent"); if (IS_ERR(char_class)) { ret = -ENOMEM; pr_err("Failed to create sysevent_dev class\n"); goto err_class; } ret = atomic_notifier_chain_register(&panic_notifier_list, &panic_nb); if (ret) goto err_soc; return 0; err_soc: class_destroy(char_class); err_class: bus_unregister(&sysevent_bus_type); err_bus: destroy_workqueue(sysevent_wq); return ret; } arch_initcall(sysevent_init); MODULE_AUTHOR("Hosung Kim "); MODULE_AUTHOR("Changki Kim "); MODULE_AUTHOR("Donghyeok Choe "); MODULE_DESCRIPTION("System Event Framework Driver"); MODULE_LICENSE("GPL v2");