Subject: Kernel module to directly write to the parallel port registers From: Carsten Emde <C.Emde@osadl.org> Date: Sat, 15 Dec 2012 00:00:28 +0100 It sometimes is required to directly signal a computer state at an output port without using a driver, e.g. in a crashed system that is no longer able to handle interrupts but still has some kind of life in it. The parallel port is ideally suitable for this purpose. This patch provides a driver interface to facilitate determination of the prallel port registers and to visualize system calls, hardware IRQs or software IRQs as additional hints in a post-mortem diagnosis of a system crash. Signed-off-by: Carsten Emde <C.Emde@osadl.org> --- drivers/misc/Kconfig | 66 +++++ drivers/misc/Makefile | 1 drivers/misc/setparport.c | 468 ++++++++++++++++++++++++++++++++++++++++++ fs/super.c | 1 fs/sync.c | 2 include/linux/hardirq.h | 4 include/linux/interrupt.h | 2 kernel/irq/handle.c | 4 kernel/irq/manage.c | 7 kernel/sched/core.c | 1 kernel/trace/trace_syscalls.c | 3 11 files changed, 559 insertions(+) Index: linux-5.4.5-rt3/drivers/misc/Kconfig =================================================================== @ linux-5.4.5-rt3/drivers/misc/Kconfig:469 @ config PVPANIC a paravirtualized device provided by QEMU; it lets a virtual machine (guest) communicate panic events to the host. +config SETPARPORT_RAW + tristate "Raw output driver for parallel port" + select LOCKUP_DETECTOR + help + It sometimes is required to directly signal a specific state at + the parallel port without using a driver, e.g. in a crashed + system that still has some kind of life in it. Usage: + echo 0 .. 255 >/dev/setparport -> set output byte + echo 256 .. 511 >/dev/setparport -> or byte with output byte + echo 512 >/dev/setparport clear all output bits + echo 513 >/dev/setparport set all output bits + echo 514 >/dev/setparport invert output bits + echo 515 >/dev/setparport increment output bits + echo 516 >/dev/setparport decrement output bits + echo 517 >/dev/setparport copy status register to output bits + echo 518 >/dev/setparport copy jiffies LSBs >> 10 to output bits + In addition, this driver is used as a callback in the NMI handler. + If installed, it allows to monitor NMI activity, e.g. using LEDs + connected to the parallel port. The module parameter "nmicode" is + then used to define the code to be sent at every NMI call, e.g. + to increment the 8-bit number at the parallel port at every NMI + modprobe setparport nmicode=515 + or + echo 515 >/sys/module/setparport/parameters/nmicode + + The four input lines can also be used to request specific actions; + defaults are enabled, if the parameter actions=yes is given: + S4: Show states (SysRq-T) + S5: Sync (SysRq-S) + S6: Unmount (SysRq-U) + S7: Boot (SysRq-B) + Please note that this is a simple polling mechanism; you need to + press the button during execution of the NMI callback. This was + deliberately implemented this way, in order to keep it + functional even if the entire IRQ subsystem is no longer working. + The only prerequisite is a working memory mapping of the parallel + port's IO region. If you want to let the NMI execute individual + debug actions, they must be programmed into drivers/misc/setparport.c + + Last not least, this driver can be used to output the LSB of the + most recent syscall, hardware IRQ or software IRQ vector at the + parallel port which may provide useful post-mortem information in + case of a system crash. + System call: + modprobe setparport sysenter=1 sysexit=0 + or + echo 1 >/sys/module/setparport/parameters/sysenter + echo 0 >/sys/module/setparport/parameters/sysexit + Hardware IRQ number: + modprobe setparport irqenter=1, irqexit=384 + or + echo 1 >/sys/module/setparport/parameters/irqenter + echo 384 >/sys/module/setparport/parameters/irqexit + Software IRQ vector number: + modprobe setparport sirqenter=1 sirqexit=384 + or + echo 1 >/sys/module/setparport/parameters/sirqenter + echo 384 >/sys/module/setparport/parameters/sirqexit + + If configured as a built-in kernel module, the following kernel + command line parameters apply: + setparport=<actions>,<nmicode> + setparportirq=<irqenter>,<irqexit> + setparportsirq=<sirqenter>,<sirqexit> + setparportsyscall=<sysenter>,<sysexit> + source "drivers/misc/c2port/Kconfig" source "drivers/misc/eeprom/Kconfig" source "drivers/misc/cb710/Kconfig" Index: linux-5.4.5-rt3/drivers/misc/Makefile =================================================================== --- linux-5.4.5-rt3.orig/drivers/misc/Makefile +++ linux-5.4.5-rt3/drivers/misc/Makefile @ linux-5.4.5-rt3/drivers/misc/Kconfig:39 @ obj-$(CONFIG_C2PORT) += c2port/ obj-$(CONFIG_HMC6352) += hmc6352.o obj-y += eeprom/ obj-y += cb710/ +obj-$(CONFIG_SETPARPORT_RAW) += setparport.o obj-$(CONFIG_VMWARE_BALLOON) += vmw_balloon.o obj-$(CONFIG_PCH_PHUB) += pch_phub.o obj-y += ti-st/ Index: linux-5.4.5-rt3/drivers/misc/setparport.c =================================================================== --- /dev/null +++ linux-5.4.5-rt3/drivers/misc/setparport.c @ linux-5.4.5-rt3/drivers/misc/Kconfig:4 @ +/* + * setparport - set bits of the parallel port by writing immediately + * into the controller's data register + * + * Copyright (C) 2012 Carsten Emde <C.Emde@osadl.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation either version 2 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include <linux/module.h> +#include <linux/device.h> +#include <linux/sched.h> +#include <linux/sched/clock.h> +#include <linux/sched/debug.h> +#include <linux/cpumask.h> +#include <linux/time.h> +#include <linux/types.h> +#include <linux/errno.h> +#include <linux/miscdevice.h> +#include <linux/proc_fs.h> +#include <linux/spinlock.h> +#include <linux/ioport.h> +#include <linux/init.h> +#include <linux/uaccess.h> +#include <linux/io.h> +#include <linux/interrupt.h> +#include <linux/reboot.h> +#include <linux/atomic.h> +#include <linux/lp.h> +#include <linux/kernel.h> + +#include <trace/events/syscalls.h> +#include <trace/events/irq.h> + +#define SETPARPORT_MINOR MISC_DYNAMIC_MINOR + +#define MAX_NMICODE 519 +#define DRV_NAME "setparport" +#define PR_DRV_NAME DRV_NAME ": " +#define minof(a, b) (((a) < (b)) ? (a) : (b)) + +static unsigned int ioport; + +static void __write(int bit) +{ + if (bit >= 0 && bit < 256) + outb(bit, ioport); /* set */ + else if (bit >= 256 && bit < 512) { + outb(inb(ioport) | bit, ioport); /* or */ + } else { + switch (bit) { + case 512: + outb(0x0, ioport); /* clear all */ + break; + case 513: + outb(0xff, ioport); /* set all */ + break; + case 514: + outb(~inb(ioport), ioport); /* invert */ + break; + case 515: + outb(inb(ioport) + 1, ioport); /* incr */ + break; + case 516: + outb(inb(ioport) - 1, ioport); /* decr */ + break; + case 517: + outb(inb(ioport + 1), ioport); /* status reg */ + break; + case 518: + outb(inb(ioport + 2), ioport); /* control reg */ + break; + case MAX_NMICODE: { + unsigned long myjiffies = jiffies; + + outb(myjiffies >> 10, ioport); + break; + } + } + } +} + + +static unsigned long get_timestamp_ms(int cpu) +{ + return cpu_clock(cpu) >> 27LL; /* resolution roughly 1 ms */ +} + +static unsigned long lastaccess; +static int lastcpu; +static ssize_t +setparport_read(struct file *fp, char __user *buf, size_t len, loff_t *off) +{ + if (!ioport) + return -ENODEV; + + if (get_timestamp_ms(lastcpu) > lastaccess) { + char outstr[8]; + + snprintf(outstr, sizeof(outstr), "0x%02x\n", inb(ioport)); + len = copy_to_user(buf, outstr, minof(strlen(outstr), len)); + if (!len) { + lastcpu = raw_smp_processor_id(); + lastaccess = get_timestamp_ms(lastcpu); + len = strlen(outstr); + } + } else + len = 0; + + return len; +} + +static ssize_t +setparport_write(struct file *file, const char __user *buf, size_t count, + loff_t *ppos) +{ + int len, bit; + + if (!ioport) + return -ENODEV; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + len = kstrtoint_from_user(buf, count, 10, &bit); + if (!len) { + __write(bit); + return count; + } + + return len; +} + +static int +setparport_open(struct inode *inode, struct file *file) +{ + return 0; +} + +static int +setparport_release(struct inode *inode, struct file *file) +{ + return 0; +} + +static const struct file_operations setparport_fops = { + .owner = THIS_MODULE, + .llseek = no_llseek, + .open = setparport_open, + .read = setparport_read, + .write = setparport_write, + .release = setparport_release, +}; + +static struct miscdevice setparport_dev = { + SETPARPORT_MINOR, + DRV_NAME, + &setparport_fops +}; + +static long nmicode = -1; +static bool actions; + +#define biton(s, b) ((s)&(1<<(b))) +#define bitoff(s, b) (!biton(s, b)) +#define S4(s) bitoff(s, 4) +#define S5(s) bitoff(s, 5) +#define S6(s) bitoff(s, 6) +#define S7(s) biton(s, 7) + +static void doitfromkernel(void) +{ + int status = inb(ioport + 1); + + if (S4(status)) { + if (actions) { + pr_info(PR_DRV_NAME "Show state\n"); + show_state(); + } else + pr_info(PR_DRV_NAME "S4\n"); + } + if (S5(status)) { + if (actions) { + emergency_sync(); + pr_info(PR_DRV_NAME "Sync done\n"); + } else + pr_info(PR_DRV_NAME "S5\n"); + } + if (S6(status)) { + if (actions) { + emergency_remount(); + pr_info(PR_DRV_NAME "Remount RO done\n"); + } else + pr_info(PR_DRV_NAME "S6\n"); + } + if (S7(status)) { + if (actions) { + pr_info(PR_DRV_NAME "Restarting\n"); + lockdep_off(); + local_irq_enable(); + emergency_restart(); + } else + pr_info(PR_DRV_NAME "S7\n"); + } + + if (nmicode != -1) + __write(nmicode); +} + +#ifdef MODULE +module_param(actions, bool, 0600); +MODULE_PARM_DESC(actions, "enable default debug actions:\n" +" S4: show task states (SysRq-T), S5: sync disks (SysRq-S)\n" +" S6: unmount disks (SysRq-U), S7: reboot (SysRq-B"); +module_param(nmicode, long, 0600); +MODULE_PARM_DESC(nmicode, "code to send by NMI handler:\n" +" -1: none, 0-255: set, 256-511: or, 512: clear all, 513: set all,\n" +" 514: invert, 515: incr, 516: decr, 517: ctrl reg, 518: jiffies"); +#else +static int __init parse_setparport(char *str, bool *arg1, long *arg2, + const char *what) +{ + int ret; + unsigned long val; + char *sep; + + ret = kstrtoul(str, 0, &val); + if (ret < 0 || val > 1) { + pr_warn(PR_DRV_NAME "%s=%s not understood\n", what, str); + return 1; + } + *arg1 = val; + sep = strchr(str, ','); + if (sep++) { + ret = kstrtoul(sep, 0, &val); + if (ret < 0 || val > MAX_NMICODE) { + pr_warn(PR_DRV_NAME + "%s 2nd arg %s not understood\n", what, sep); + return 1; + } + *arg2 = (long) val; + } + return 1; +} + +static int __init setparport_setup(char *str) +{ + return parse_setparport(str, &actions, &nmicode, "setparport"); +} +__setup("setparport=", setparport_setup); + +void setparport(int val) +{ + if (ioport) + __write(val); +} +EXPORT_SYMBOL_GPL(setparport); +#endif + +#ifdef CONFIG_TRACEPOINTS +static bool irqenter; +static notrace void probe_irq_enter(void *v, int irq, struct irqaction *action) +{ + if (irqenter) + __write(irq); +} + +static long irqexit = -1; +static notrace void probe_irq_exit(void *v, int irq, struct irqaction *action, + int ret) +{ + if (irqexit != -1) + __write(irqexit); +} + +static bool sirqenter; +static notrace void probe_sirq_enter(void *v, unsigned int vec_nr) +{ + if (sirqenter) + __write(vec_nr); +} + +static long sirqexit = -1; +static notrace void probe_sirq_exit(void *v, unsigned int vec_nr) +{ + if (sirqexit != -1) + __write(sirqexit); +} + +#ifdef MODULE +module_param(irqenter, bool, 0600); +MODULE_PARM_DESC(irqenter, "output irq number of most recent irq handler"); +module_param(irqexit, long, 0600); +MODULE_PARM_DESC(irqexit, "byte to output when exiting from irq handler"); +module_param(sirqenter, bool, 0600); +MODULE_PARM_DESC(sirqenter, "output vector of most recent soft irq handler"); +module_param(sirqexit, long, 0600); +MODULE_PARM_DESC(sirqexit, "byte to output when exiting from soft irq handler"); +#else +static int __init setparportirq_setup(char *str) +{ + return parse_setparport(str, &irqenter, &irqexit, "setparportirq"); +} +__setup("setparportirq=", setparportirq_setup); + +static int __init setparportsirq_setup(char *str) +{ + return parse_setparport(str, &sirqenter, &sirqexit, "setparportsirq"); +} +__setup("setparportsirq=", setparportsirq_setup); +#endif + +#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS +static bool sysenter; +static notrace void probe_syscall_enter(void *v, struct pt_regs *regs, long ret) +{ + if (sysenter) + __write(ret & 0xff); +} + +static long sysexit = -1; +static notrace void probe_syscall_exit(void *v, struct pt_regs *regs, long ret) +{ + if (sysexit != -1) + __write(sysexit); +} + +#ifdef MODULE +module_param(sysenter, bool, 0600); +MODULE_PARM_DESC(sysenter, "output LSB of most recent syscall"); +module_param(sysexit, long, 0600); +MODULE_PARM_DESC(sysexit, "byte to output when exiting from syscall"); +#else +static int __init setparportsyscall_setup(char *str) +{ + return parse_setparport(str, &sysenter, &sysexit, "setparportsyscall"); +} + +__setup("setparportsyscall=", setparportsyscall_setup); +#endif /* MODULE */ +#endif /* CONFIG_HAVE_SYSCALL_TRACEPOINTS */ +#endif /* CONFIG_TRACEPOINTS */ + +static bool irqenable; +#ifdef MODULE +module_param(irqenable, bool, 0600); +MODULE_PARM_DESC(irqenable, "enable interrupts, e.g. at rising edge of pin 10"); +#else +static int __init setparportirqenable_setup(char *str) +{ + int par; + + if (get_option(&str, &par)) + irqenable = par; + return 1; +} +__setup("setparportirqenable=", setparportirqenable_setup); +#endif + +static int wrtest(unsigned char pat) +{ + unsigned char test, old = inb(ioport); + + outb(pat, ioport); + test = inb(ioport); + outb(old, ioport); + return test == pat; +} + +static DEFINE_RWLOCK(resource_lock); +static int __init setparport_init(void) +{ + int ret; + struct resource *root = &ioport_resource, *res; + char *ourport = "parport"; + + read_lock(&resource_lock); + for (res = root->child; res; ) { + if (!strncmp(res->name, ourport, strlen(ourport))) { + ioport = res->start; + if (wrtest(0x55) && wrtest(0xaa)) + break; + ioport = 0; + } + if (res->child) + res = res->child; + else { + while (!res->sibling && res->parent) + res = res->parent; + res = res->sibling; + } + } + read_unlock(&resource_lock); + + if (ioport) { + pr_info(PR_DRV_NAME "found parport's ioport 0x%04x\n", ioport); + ret = misc_register(&setparport_dev); + if (ret) + pr_err(PR_DRV_NAME "can't register device\n"); + else { + unsigned char inten = 0; + + pr_info(PR_DRV_NAME "misc device %d\n", + setparport_dev.minor); + + /* clear all output data bits */ + outb(0, ioport); + + if (irqenable) + inten = LP_PINTEN; + /* also sets all control pins to +5V */ + outb(LP_PINITP|inten, ioport + 2); + +#ifdef CONFIG_TRACEPOINTS + register_trace_irq_handler_entry(probe_irq_enter, NULL); + register_trace_irq_handler_exit(probe_irq_exit, NULL); + register_trace_softirq_entry(probe_sirq_enter, NULL); + register_trace_softirq_exit(probe_sirq_exit, NULL); +#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS + register_trace_sys_enter(probe_syscall_enter, NULL); + register_trace_sys_exit(probe_syscall_exit, NULL); +#endif +#endif + set_nmicallback(doitfromkernel); + } + } else { + pr_err(PR_DRV_NAME "no parallel port found\n"); + ret = -ENODEV; + } + return ret; +} + +static void __exit setparport_exit(void) +{ + set_nmicallback(NULL); +#ifdef CONFIG_TRACEPOINTS + unregister_trace_irq_handler_entry(probe_irq_enter, NULL); + unregister_trace_irq_handler_exit(probe_irq_exit, NULL); + unregister_trace_softirq_entry(probe_sirq_enter, NULL); + unregister_trace_softirq_exit(probe_sirq_exit, NULL); +#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS + unregister_trace_sys_enter(probe_syscall_enter, NULL); + unregister_trace_sys_exit(probe_syscall_exit, NULL); +#endif +#endif + misc_deregister(&setparport_dev); + pr_info(PR_DRV_NAME "exit\n"); +} + +module_init(setparport_init); +module_exit(setparport_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Carsten Emde <C.Emde@osadl.org>"); +MODULE_DESCRIPTION("Install NMI call and set/get parallel port w/o device IRQ"); Index: linux-5.4.5-rt3/fs/super.c =================================================================== --- linux-5.4.5-rt3.orig/fs/super.c +++ linux-5.4.5-rt3/fs/super.c @ linux-5.4.5-rt3/drivers/misc/Kconfig:1059 @ void emergency_thaw_all(void) schedule_work(work); } } +EXPORT_SYMBOL(emergency_remount); static DEFINE_IDA(unnamed_dev_ida); Index: linux-5.4.5-rt3/fs/sync.c =================================================================== --- linux-5.4.5-rt3.orig/fs/sync.c +++ linux-5.4.5-rt3/fs/sync.c @ linux-5.4.5-rt3/drivers/misc/Kconfig:156 @ void emergency_sync(void) schedule_work(work); } } +EXPORT_SYMBOL(emergency_sync); + /* * sync a single super Index: linux-5.4.5-rt3/include/linux/hardirq.h =================================================================== --- linux-5.4.5-rt3.orig/include/linux/hardirq.h +++ linux-5.4.5-rt3/include/linux/hardirq.h @ linux-5.4.5-rt3/drivers/misc/Kconfig:68 @ extern void irq_exit(void); #define arch_nmi_exit() do { } while (0) #endif +extern void (*nmicallback)(void); + #define nmi_enter() \ do { \ arch_nmi_enter(); \ @ linux-5.4.5-rt3/drivers/misc/Kconfig:79 @ extern void irq_exit(void); preempt_count_add(NMI_OFFSET + HARDIRQ_OFFSET); \ rcu_nmi_enter(); \ trace_hardirq_enter(); \ + if (nmicallback) \ + nmicallback(); \ } while (0) #define nmi_exit() \ Index: linux-5.4.5-rt3/include/linux/interrupt.h =================================================================== --- linux-5.4.5-rt3.orig/include/linux/interrupt.h +++ linux-5.4.5-rt3/include/linux/interrupt.h @ linux-5.4.5-rt3/drivers/misc/Kconfig:757 @ extern int arch_early_irq_init(void); #define __softirq_entry \ __attribute__((__section__(".softirqentry.text"))) +void set_nmicallback(void (*new_nmicallback)(void)); + #endif Index: linux-5.4.5-rt3/kernel/irq/handle.c =================================================================== --- linux-5.4.5-rt3.orig/kernel/irq/handle.c +++ linux-5.4.5-rt3/kernel/irq/handle.c @ linux-5.4.5-rt3/drivers/misc/Kconfig:18 @ #include <linux/kernel_stat.h> #include <trace/events/irq.h> +EXPORT_TRACEPOINT_SYMBOL_GPL(irq_handler_entry); +EXPORT_TRACEPOINT_SYMBOL_GPL(irq_handler_exit); +EXPORT_TRACEPOINT_SYMBOL_GPL(softirq_entry); +EXPORT_TRACEPOINT_SYMBOL_GPL(softirq_exit); #include "internals.h" Index: linux-5.4.5-rt3/kernel/irq/manage.c =================================================================== --- linux-5.4.5-rt3.orig/kernel/irq/manage.c +++ linux-5.4.5-rt3/kernel/irq/manage.c @ linux-5.4.5-rt3/drivers/misc/Kconfig:2726 @ int irq_set_irqchip_state(unsigned int i return err; } EXPORT_SYMBOL_GPL(irq_set_irqchip_state); + +void (*nmicallback)(void); +void set_nmicallback(void (*new_nmicallback)(void)) +{ + nmicallback = new_nmicallback; +} +EXPORT_SYMBOL_GPL(set_nmicallback); Index: linux-5.4.5-rt3/kernel/sched/core.c =================================================================== --- linux-5.4.5-rt3.orig/kernel/sched/core.c +++ linux-5.4.5-rt3/kernel/sched/core.c @ linux-5.4.5-rt3/drivers/misc/Kconfig:5798 @ static void do_sched_yield(void) schedule(); } +EXPORT_SYMBOL(show_state_filter); SYSCALL_DEFINE0(sched_yield) { Index: linux-5.4.5-rt3/kernel/trace/trace_syscalls.c =================================================================== --- linux-5.4.5-rt3.orig/kernel/trace/trace_syscalls.c +++ linux-5.4.5-rt3/kernel/trace/trace_syscalls.c @ linux-5.4.5-rt3/drivers/misc/Kconfig:15 @ #include "trace_output.h" #include "trace.h" +EXPORT_TRACEPOINT_SYMBOL_GPL(sys_enter); +EXPORT_TRACEPOINT_SYMBOL_GPL(sys_exit); + static DEFINE_MUTEX(syscall_trace_lock); static int syscall_enter_register(struct trace_event_call *event,