From: Sasha Levin <sashal-AT-kernel.org> To: corbet-AT-lwn.net, akpm-AT-linux-foundation.org Subject: [PATCH] killswitch: add per-function short-circuit mitigation primitive Date: Thu, 07 May 2026 03:05:45 -0400 Message-ID: <[email protected]> Cc: skhan-AT-linuxfoundation.org, linux-doc-AT-vger.kernel.org, linux-kernel-AT-vger.kernel.org, linux-kselftest-AT-vger.kernel.org, gregkh-AT-linuxfoundation.org, Sasha Levin <sashal-AT-kernel.org>
When a (security) issue goes public, fleets stay exposed until a patched kernel is built, distributed, and rebooted into. For many such issues the simplest mitigation is to stop calling the buggy function. Killswitch provides that. An admin writes: echo "engage af_alg_sendmsg -1" \ > /sys/kernel/security/killswitch/control After this, af_alg_sendmsg() returns -EPERM on every call without running its body. The mitigation takes effect immediately, and is dropped on the next reboot. A lot of recent kernel issues sit in code paths most installs only have enabled to support a relative minority of users: AF_ALG, ksmbd, nf_tables, vsock, ax25, and friends. For most users, the cost of "this socket family stops working for the day" is much smaller than the cost of running a known vulnerable kernel until the fix land. Assisted-by: Claude:claude-opus-4-7 Signed-off-by: Sasha Levin <[email protected]> --- Documentation/admin-guide/index.rst | 1 + Documentation/admin-guide/killswitch.rst | 159 ++++ Documentation/admin-guide/tainted-kernels.rst | 8 + MAINTAINERS | 11 + include/linux/killswitch.h | 19 + include/linux/panic.h | 3 +- init/Kconfig | 2 + kernel/Kconfig.killswitch | 31 + kernel/Makefile | 1 + kernel/killswitch.c | 798 ++++++++++++++++++ kernel/panic.c | 1 + lib/Kconfig.debug | 13 + lib/Makefile | 1 + lib/test_killswitch.c | 85 ++ tools/testing/selftests/Makefile | 1 + tools/testing/selftests/killswitch/.gitignore | 1 + tools/testing/selftests/killswitch/Makefile | 8 + .../selftests/killswitch/cve_31431_test.c | 162 ++++ .../selftests/killswitch/killswitch_test.sh | 147 ++++ 19 files changed, 1451 insertions(+), 1 deletion(-) create mode 100644 Documentation/admin-guide/killswitch.rst create mode 100644 include/linux/killswitch.h create mode 100644 kernel/Kconfig.killswitch create mode 100644 kernel/killswitch.c create mode 100644 lib/test_killswitch.c create mode 100644 tools/testing/selftests/killswitch/.gitignore create mode 100644 tools/testing/selftests/killswitch/Makefile create mode 100644 tools/testing/selftests/killswitch/cve_31431_test.c create mode 100755 tools/testing/selftests/killswitch/killswitch_test.sh diff --git a/Documentation/admin-guide/index.rst b/Documentation/admin-guide/index.rst index cd28dfe91b060..ca37dd70f108d 100644 --- a/Documentation/admin-guide/index.rst +++ b/Documentation/admin-guide/index.rst @@ -70,6 +70,7 @@ problems and bugs in particular. bug-hunting bug-bisect tainted-kernels + killswitch ramoops dynamic-debug-howto init diff --git a/Documentation/admin-guide/killswitch.rst b/Documentation/admin-guide/killswitch.rst new file mode 100644 index 0000000000000..cb967ec348fdc --- /dev/null +++ b/Documentation/admin-guide/killswitch.rst @@ -0,0 +1,159 @@ +.. SPDX-License-Identifier: GPL-2.0 +.. +.. Copyright (C) 2026 Sasha Levin <[email protected]> + +============ +Killswitch +============ + +Killswitch lets a privileged operator make a chosen kernel function +return a fixed value without executing its body, as a temporary +mitigation for a security bug while a real fix is being prepared. + +The function returns the operator-supplied value and nothing else +runs in its place. There is no allowlist, no return-type check; if +the kprobe layer accepts the symbol, killswitch engages it. Once +engaged, the change is in effect on every CPU until ``disengage`` is +written or the system reboots. + +Configuration +============= + +``CONFIG_KILLSWITCH`` + Enables the feature. Depends on ``SECURITYFS``, ``KPROBES`` (with + ftrace support), and ``FUNCTION_ERROR_INJECTION``. + +The interface +============= + +:: + + /sys/kernel/security/killswitch/ + engaged RO currently-engaged functions + control WO command sink + taint RO 0 or 1 + fn/<name>/ per-function directory, created on engage + retval RW return value + hits RO per-cpu summed call count + +Three commands are accepted by ``control``:: + + engage <symbol> <retval> + disengage <symbol> + disengage_all + +Each engage and disengage emits a single ``KERN_WARNING`` line to +dmesg with the symbol, retval, hit count (on disengage), and the +operator's identity (uid/auid/sessionid/comm, or ``source=cmdline``). + +Engagement is rejected when: + +* the symbol is unknown, in a non-traceable section, on the kprobe + blacklist, or otherwise refused by ``register_kprobe`` (the error + from the kprobe layer is logged and returned to userspace); +* the symbol is already engaged (``-EBUSY``); +* the operator does not hold ``CAP_SYS_ADMIN``. + +Whatever value the operator writes is what the function returns. +Writing the wrong type or wrong value lands in the caller as-is. + +Boot parameter +============== + +``killswitch=fn1=<val>,fn2=<val>,...`` + +Parsed early; engagements are applied at the end of kernel init +once the kprobe subsystem is up. Parse failures emit a warning and +skip the offending entry; they never panic. + +Useful for fleet rollout: when an issue drops, ship the mitigation +in the bootloader / PXE config and roll the fleet through reboots +while the real fix is being prepared. + +Tainting +======== + +The first successful engagement (runtime or boot-time) sets +``TAINT_KILLSWITCH`` (bit 20, char ``H``). The taint persists across +``disengage`` until reboot, so an oops on a killswitch-modified +kernel is identifiable from the banner: ``Tainted: ... H`` tells a +maintainer to consult ``engaged`` before further triage. + +Module unload +============= + +If a module containing an engaged target is unloaded, killswitch +auto-disengages the entry and emits a ``KERN_WARNING`` so the loss +of mitigation is visible. Reloading the module does not silently +re-arm the killswitch; the operator re-engages explicitly. + +Choosing the right target +========================= + +A function that *looks* skippable may be relied on by callers for a +side effect (a lock the caller releases, a refcount the caller +drops, a scatterlist the caller consumes). The rule of thumb: + + Pick the **highest-level** entry point that contains the bug. + +That gives callers no chance to dereference half-initialised state +from a function whose body was skipped. Two illustrative examples +from ``crypto/af_alg.c``: + +Anti-pattern: ``af_alg_count_tsgl`` +----------------------------------- + +``af_alg_count_tsgl()`` returns ``unsigned int`` (the number of TX +SG entries). Engaging it with retval ``0`` causes the caller in +``algif_aead.c`` to allocate a 1-entry scatterlist (its +``if (!entries) entries = 1`` guard) and then walk the *real* TX +SGL into that undersized destination via ``af_alg_pull_tsgl``, +producing out-of-bounds writes. **Killswitching here introduces a +worse bug than the one being mitigated.** + +Anti-pattern: ``af_alg_pull_tsgl`` +---------------------------------- + +``af_alg_pull_tsgl()`` returns ``void``, so any retval is accepted. +But its caller depends on the per-request SGL being filled in. +Skipping the body leaves the per-request SGL with NULL pages; the +next-stage ``memcpy_sglist`` dereferences them and the kernel +oopses. + +Correct pattern: ``af_alg_sendmsg`` +----------------------------------- + +``af_alg_sendmsg()`` is the highest-level entry into the AF_ALG +send path. Engaging it with retval ``-EPERM`` causes every send +attempt to return -EPERM to userspace; no caller ever sees +half-initialised state, and any AF_ALG-reachable bug downstream of +``sendmsg`` is unreachable until the killswitch is disengaged. + +The canonical pattern: pick a syscall-handler-shaped function whose +return value already encodes "this operation didn't happen", and +let userspace handle the error as it would any other failed +syscall. + +Safety notes +============ + +* In-flight calls during ``write()`` to ``control`` may run either + the original body or the override. The override is ``return X``, + which has no preconditions to violate. +* SMP visibility comes from ``text_poke_bp()``. ``write()`` to + ``control`` returns only after every CPU sees the new path. +* The ftrace ops unregister waits for in-flight pre-handlers, so + freeing the engagement attribute on disengage is safe. +* Inline functions, freed ``__init`` symbols, and anything compiled + away cannot be killswitched. ``register_kprobe`` rejects them + with whatever error the kprobe layer chooses. + +Diagnostics +=========== + +Per-call hits are aggregated in a per-cpu counter readable at +``/sys/kernel/security/killswitch/fn/<name>/hits``. Per-hit logging +is not provided to avoid log storms on hot paths. + +A ``KILLSWITCH`` entry appears in the kernel taint vector once any +engagement succeeds (also visible as ``H`` in the oops banner). diff --git a/Documentation/admin-guide/tainted-kernels.rst b/Documentation/admin-guide/tainted-kernels.rst index 9ead927a37c0f..71a6e3364eddc 100644 --- a/Documentation/admin-guide/tainted-kernels.rst +++ b/Documentation/admin-guide/tainted-kernels.rst @@ -102,6 +102,7 @@ Bit Log Number Reason that got the kernel tainted 17 _/T 131072 kernel was built with the struct randomization plugin 18 _/N 262144 an in-kernel test has been run 19 _/J 524288 userspace used a mutating debug operation in fwctl + 20 _/H 1048576 killswitch override engaged (function short-circuited) === === ====== ======================================================== Note: The character ``_`` is representing a blank in this table to make reading @@ -189,3 +190,10 @@ More detailed explanation for tainting 19) ``J`` if userspace opened /dev/fwctl/* and performed a FWTCL_RPC_DEBUG_WRITE to use the devices debugging features. Device debugging features could cause the device to malfunction in undefined ways. + + 20) ``H`` if the killswitch primitive (see + Documentation/admin-guide/killswitch.rst) has been engaged on at least + one function. The kernel is no longer running its source: at least one + function has been short-circuited to return a fixed value. The taint + persists across ``disengage`` until the next reboot — once the running + image has been modified, oops triage must reflect that. diff --git a/MAINTAINERS b/MAINTAINERS index 882214b0e7db5..61851ef1d9b1c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14347,6 +14347,17 @@ F: lib/Kconfig.kmsan F: mm/kmsan/ F: scripts/Makefile.kmsan +KILLSWITCH (function short-circuit mitigation) +M: Sasha Levin <[email protected]> +L: [email protected] +S: Maintained +F: Documentation/admin-guide/killswitch.rst +F: include/linux/killswitch.h +F: kernel/Kconfig.killswitch +F: kernel/killswitch.c +F: lib/test_killswitch.c +F: tools/testing/selftests/killswitch/ + KPROBES M: Naveen N Rao <[email protected]> M: "David S. Miller" <[email protected]> diff --git a/include/linux/killswitch.h b/include/linux/killswitch.h new file mode 100644 index 0000000000000..c18515bec09f1 --- /dev/null +++ b/include/linux/killswitch.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2026 Sasha Levin <[email protected]> + */ +#ifndef _LINUX_KILLSWITCH_H +#define _LINUX_KILLSWITCH_H + +#ifdef CONFIG_KILLSWITCH +int killswitch_engage(const char *symbol, long retval); +int killswitch_disengage(const char *symbol); +bool killswitch_is_engaged(const char *symbol); +#else +static inline int killswitch_engage(const char *symbol, long retval) +{ return -ENOSYS; } +static inline int killswitch_disengage(const char *symbol) { return -ENOSYS; } +static inline bool killswitch_is_engaged(const char *symbol) { return false; } +#endif + +#endif /* _LINUX_KILLSWITCH_H */ diff --git a/include/linux/panic.h b/include/linux/panic.h index f1dd417e54b29..6699261a61f13 100644 --- a/include/linux/panic.h +++ b/include/linux/panic.h @@ -88,7 +88,8 @@ static inline void set_arch_panic_timeout(int timeout, int arch_default_timeout) #define TAINT_RANDSTRUCT 17 #define TAINT_TEST 18 #define TAINT_FWCTL 19 -#define TAINT_FLAGS_COUNT 20 +#define TAINT_KILLSWITCH 20 +#define TAINT_FLAGS_COUNT 21 #define TAINT_FLAGS_MAX ((1UL << TAINT_FLAGS_COUNT) - 1) struct taint_flag { diff --git a/init/Kconfig b/init/Kconfig index 2937c4d308aec..5368dd4b5c65b 100644 --- a/init/Kconfig +++ b/init/Kconfig @@ -2278,6 +2278,8 @@ config ASN1 source "kernel/Kconfig.locks" +source "kernel/Kconfig.killswitch" + config ARCH_HAS_NON_OVERLAPPING_ADDRESS_SPACE bool diff --git a/kernel/Kconfig.killswitch b/kernel/Kconfig.killswitch new file mode 100644 index 0000000000000..067d41087e8da --- /dev/null +++ b/kernel/Kconfig.killswitch @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Killswitch: per-function short-circuit mitigation primitive. +# +# Copyright (C) 2026 Sasha Levin <[email protected]> +# + +config KILLSWITCH + bool "Killswitch: short-circuit a kernel function as a CVE mitigation" + depends on SECURITYFS + depends on KPROBES && HAVE_KPROBES_ON_FTRACE + depends on HAVE_FUNCTION_ERROR_INJECTION + select FUNCTION_ERROR_INJECTION + help + Provide an admin-facing mechanism to make a chosen kernel function + return a fixed value without executing its body, as a temporary + mitigation for a security bug before a real fix is available. + + Operators write "engage <symbol> <retval> [reason]" to + /sys/kernel/security/killswitch/control. The function entry is + redirected via a kprobe whose pre-handler sets the chosen return + value and short-circuits the call. There is no allowlist, + denylist, or return-type validation: if the kprobe layer accepts + the symbol the engagement proceeds, otherwise its error is + returned to userspace. + + This is *not* livepatch: there is no replacement implementation, + the function simply returns the chosen value. Engaging a killswitch + taints the kernel (TAINT_KILLSWITCH, 'H'). Requires CAP_SYS_ADMIN. + + If unsure, say N. diff --git a/kernel/Makefile b/kernel/Makefile index 6785982013dce..b3e408d9f275e 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -100,6 +100,7 @@ obj-$(CONFIG_GCOV_KERNEL) += gcov/ obj-$(CONFIG_KCOV) += kcov.o obj-$(CONFIG_KPROBES) += kprobes.o obj-$(CONFIG_FAIL_FUNCTION) += fail_function.o +obj-$(CONFIG_KILLSWITCH) += killswitch.o obj-$(CONFIG_KGDB) += debug/ obj-$(CONFIG_DETECT_HUNG_TASK) += hung_task.o obj-$(CONFIG_LOCKUP_DETECTOR) += watchdog.o diff --git a/kernel/killswitch.c b/kernel/killswitch.c new file mode 100644 index 0000000000000..6b3e2982e1c5c --- /dev/null +++ b/kernel/killswitch.c @@ -0,0 +1,798 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Per-function short-circuit mitigation. + * + * Copyright (C) 2026 Sasha Levin <[email protected]> + * + * Engaging a killswitch installs a kprobe at the function's entry + * whose pre-handler sets the return register and skips the body via + * override_function_with_return(). Operator interface lives at + * /sys/kernel/security/killswitch/. + */ + +#include <linux/audit.h> +#include <linux/capability.h> +#include <linux/cred.h> +#include <linux/ctype.h> +#include <linux/error-injection.h> +#include <linux/init.h> +#include <linux/killswitch.h> +#include <linux/kprobes.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/notifier.h> +#include <linux/panic.h> +#include <linux/percpu.h> +#include <linux/printk.h> +#include <linux/refcount.h> +#include <linux/sched.h> +#include <linux/security.h> +#include <linux/seq_file.h> +#include <linux/slab.h> +#include <linux/string.h> +#include <linux/uaccess.h> +#include <linux/uidgid.h> + +struct ks_attr { + struct list_head list; + struct kprobe kp; + atomic_long_t retval; + /* false once disengaged; per-fn file ops then return -EIDRM. */ + bool engaged; + unsigned long __percpu *hits; + struct dentry *dir; + /* engaged_list holds one ref; each open per-fn fd holds one. */ + refcount_t refcnt; +}; + +static DEFINE_MUTEX(ks_lock); +static LIST_HEAD(ks_engaged_list); +static struct dentry *ks_root_dir; +static struct dentry *ks_fn_dir; /* parent for per-fn directories */ + +/* ------------------------------------------------------------------ * + * Pre-handler: the actual override * + * ------------------------------------------------------------------ */ + +static int ks_kprobe_pre_handler(struct kprobe *kp, struct pt_regs *regs) +{ + struct ks_attr *attr = container_of(kp, struct ks_attr, kp); + + this_cpu_inc(*attr->hits); + regs_set_return_value(regs, (unsigned long)atomic_long_read(&attr->retval)); + override_function_with_return(regs); + return 1; +} +NOKPROBE_SYMBOL(ks_kprobe_pre_handler); + +/* Defined non-NULL so the kprobe layer keeps the IPMODIFY ops. */ +static void ks_kprobe_post_handler(struct kprobe *kp, struct pt_regs *regs, + unsigned long flags) +{ +} + +/* ------------------------------------------------------------------ * + * Attribute lifecycle * + * ------------------------------------------------------------------ */ + +static struct ks_attr *ks_attr_lookup(const char *symbol) +{ + struct ks_attr *attr; + + list_for_each_entry(attr, &ks_engaged_list, list) + if (!strcmp(attr->kp.symbol_name, symbol)) + return attr; + return NULL; +} + +static unsigned long ks_attr_hits(const struct ks_attr *attr) +{ + unsigned long total = 0; + int cpu; + + for_each_possible_cpu(cpu) + total += *per_cpu_ptr(attr->hits, cpu); + return total; +} + +static void ks_attr_destroy(struct ks_attr *attr) +{ + if (!attr) + return; + free_percpu(attr->hits); + kfree(attr->kp.symbol_name); + kfree(attr); +} + +static void ks_attr_get(struct ks_attr *attr) +{ + refcount_inc(&attr->refcnt); +} + +static void ks_attr_put(struct ks_attr *attr) +{ + if (attr && refcount_dec_and_test(&attr->refcnt)) + ks_attr_destroy(attr); +} + +static struct ks_attr *ks_attr_alloc(const char *symbol) +{ + struct ks_attr *attr; + + attr = kzalloc(sizeof(*attr), GFP_KERNEL); + if (!attr) + return NULL; + + attr->kp.symbol_name = kstrdup(symbol, GFP_KERNEL); + if (!attr->kp.symbol_name) + goto err; + + attr->hits = alloc_percpu(unsigned long); + if (!attr->hits) + goto err; + + attr->kp.pre_handler = ks_kprobe_pre_handler; + attr->kp.post_handler = ks_kprobe_post_handler; + INIT_LIST_HEAD(&attr->list); + refcount_set(&attr->refcnt, 1); + return attr; + +err: + ks_attr_destroy(attr); + return NULL; +} + +/* ------------------------------------------------------------------ * + * Securityfs: per-fn attribute files * + * ------------------------------------------------------------------ */ + +/* + * Look up by symbol name (the parent dentry's basename) under + * ks_lock and confirm attr->dir is the file's parent dentry. This + * binds the fd to the engagement it was opened against and avoids + * dereferencing inode->i_private, which a racing disengage may have + * freed. d_parent is stable for the open's lifetime via the file's + * dentry reference. + */ +static int ks_attr_open(struct inode *inode, struct file *file) +{ + struct dentry *parent = file->f_path.dentry->d_parent; + const char *name = parent->d_name.name; + struct ks_attr *attr; + + mutex_lock(&ks_lock); + attr = ks_attr_lookup(name); + if (attr && attr->dir == parent) + ks_attr_get(attr); + else + attr = NULL; + mutex_unlock(&ks_lock); + if (!attr) + return -ENOENT; + file->private_data = attr; + return 0; +} + +static int ks_attr_release(struct inode *inode, struct file *file) +{ + ks_attr_put(file->private_data); + file->private_data = NULL; + return 0; +} + +/* Caller must hold ks_lock. */ +static int ks_attr_check_live(const struct ks_attr *attr) +{ + return attr->engaged ? 0 : -EIDRM; +} + +static ssize_t ks_retval_read(struct file *file, char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct ks_attr *attr = file->private_data; + char buf[32]; + long val; + int ret, len; + + mutex_lock(&ks_lock); + ret = ks_attr_check_live(attr); + val = atomic_long_read(&attr->retval); + mutex_unlock(&ks_lock); + if (ret) + return ret; + len = scnprintf(buf, sizeof(buf), "%ld
", val); + return simple_read_from_buffer(ubuf, count, ppos, buf, len); +} + +static ssize_t ks_retval_write(struct file *file, const char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct ks_attr *attr = file->private_data; + char buf[32]; + long val; + int ret; + + if (count >= sizeof(buf)) + return -EINVAL; + if (copy_from_user(buf, ubuf, count)) + return -EFAULT; + buf[count] = '\0'; + strim(buf); + + ret = kstrtol(buf, 0, &val); + if (ret) + return ret; + + mutex_lock(&ks_lock); + ret = ks_attr_check_live(attr); + if (!ret) + atomic_long_set(&attr->retval, val); + mutex_unlock(&ks_lock); + + return ret ? ret : count; +} + +static const struct file_operations ks_retval_fops = { + .open = ks_attr_open, + .release = ks_attr_release, + .read = ks_retval_read, + .write = ks_retval_write, + .llseek = default_llseek, +}; + +static ssize_t ks_hits_read(struct file *file, char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct ks_attr *attr = file->private_data; + char buf[32]; + unsigned long hits; + int ret, len; + + mutex_lock(&ks_lock); + ret = ks_attr_check_live(attr); + hits = ks_attr_hits(attr); + mutex_unlock(&ks_lock); + if (ret) + return ret; + len = scnprintf(buf, sizeof(buf), "%lu
", hits); + return simple_read_from_buffer(ubuf, count, ppos, buf, len); +} + +static const struct file_operations ks_hits_fops = { + .open = ks_attr_open, + .release = ks_attr_release, + .read = ks_hits_read, + .llseek = default_llseek, +}; + +static int ks_create_attr_dir(struct ks_attr *attr) +{ + struct dentry *d; + + attr->dir = securityfs_create_dir(attr->kp.symbol_name, ks_fn_dir); + if (IS_ERR(attr->dir)) + return PTR_ERR(attr->dir); + + /* ks_attr_open looks the attr up by name; i_private is unused. */ + d = securityfs_create_file("retval", 0600, attr->dir, + NULL, &ks_retval_fops); + if (IS_ERR(d)) + goto err; + d = securityfs_create_file("hits", 0400, attr->dir, + NULL, &ks_hits_fops); + if (IS_ERR(d)) + goto err; + return 0; +err: + securityfs_remove(attr->dir); + attr->dir = NULL; + return PTR_ERR(d); +} + +/* ------------------------------------------------------------------ * + * Engage / disengage * + * ------------------------------------------------------------------ */ + +static int __ks_engage(const char *symbol, long retval, bool from_cmdline) +{ + struct ks_attr *attr; + int ret; + + if (!symbol || !*symbol) + return -EINVAL; + + mutex_lock(&ks_lock); + + if (ks_attr_lookup(symbol)) { + ret = -EBUSY; + goto out_unlock; + } + + attr = ks_attr_alloc(symbol); + if (!attr) { + ret = -ENOMEM; + goto out_unlock; + } + + atomic_long_set(&attr->retval, retval); + + ret = register_kprobe(&attr->kp); + if (ret) { + pr_warn("killswitch: register_kprobe(%s) failed: %d
", + symbol, ret); + ks_attr_put(attr); + goto out_unlock; + } + + ret = ks_create_attr_dir(attr); + if (ret) { + unregister_kprobe(&attr->kp); + ks_attr_put(attr); + goto out_unlock; + } + + list_add_tail(&attr->list, &ks_engaged_list); + attr->engaged = true; + add_taint(TAINT_KILLSWITCH, LOCKDEP_STILL_OK); + + if (from_cmdline) { + pr_warn("killswitch: engage %s=%ld source=cmdline
", + symbol, retval); + } else { + pr_warn("killswitch: engage %s=%ld uid=%u auid=%u ses=%u comm=%s
", + symbol, retval, + from_kuid(&init_user_ns, current_uid()), + from_kuid(&init_user_ns, audit_get_loginuid(current)), + audit_get_sessionid(current), + current->comm); + } + ret = 0; + +out_unlock: + mutex_unlock(&ks_lock); + return ret; +} + +int killswitch_engage(const char *symbol, long retval) +{ + return __ks_engage(symbol, retval, false); +} + +static int __ks_disengage(const char *symbol) +{ + struct ks_attr *attr; + unsigned long hits; + int ret = 0; + + mutex_lock(&ks_lock); + attr = ks_attr_lookup(symbol); + if (!attr) { + ret = -ENOENT; + goto out_unlock; + } + + unregister_kprobe(&attr->kp); + attr->engaged = false; + list_del(&attr->list); + hits = ks_attr_hits(attr); + securityfs_remove(attr->dir); + + pr_warn("killswitch: disengage %s hits=%lu uid=%u auid=%u ses=%u comm=%s
", + symbol, hits, + from_kuid(&init_user_ns, current_uid()), + from_kuid(&init_user_ns, audit_get_loginuid(current)), + audit_get_sessionid(current), + current->comm); + + /* unregister_kprobe() already waited out in-flight pre-handlers. */ + ks_attr_put(attr); + +out_unlock: + mutex_unlock(&ks_lock); + return ret; +} + +int killswitch_disengage(const char *symbol) +{ + return __ks_disengage(symbol); +} + +bool killswitch_is_engaged(const char *symbol) +{ + bool engaged; + + mutex_lock(&ks_lock); + engaged = ks_attr_lookup(symbol) != NULL; + mutex_unlock(&ks_lock); + return engaged; +} + +static void ks_disengage_all_locked(void) +{ + struct ks_attr *attr, *n; + + list_for_each_entry_safe(attr, n, &ks_engaged_list, list) { + unregister_kprobe(&attr->kp); + attr->engaged = false; + list_del(&attr->list); + securityfs_remove(attr->dir); + pr_warn("killswitch: disengage %s hits=%lu (disengage_all)
", + attr->kp.symbol_name, ks_attr_hits(attr)); + ks_attr_put(attr); + } +} + +/* ------------------------------------------------------------------ * + * Module unload: drop engagements on functions in the going module * + * ------------------------------------------------------------------ */ + +static int ks_module_notify(struct notifier_block *nb, unsigned long action, + void *data) +{ + struct module *mod = data; + struct ks_attr *attr, *n; + + if (action != MODULE_STATE_GOING) + return NOTIFY_DONE; + + mutex_lock(&ks_lock); + list_for_each_entry_safe(attr, n, &ks_engaged_list, list) { + if (!attr->kp.addr || + __module_address((unsigned long)attr->kp.addr) != mod) + continue; + + pr_warn("killswitch: %s mitigation lost: module %s unloading; re-engage after reload if still needed
", + attr->kp.symbol_name, mod->name); + unregister_kprobe(&attr->kp); + attr->engaged = false; + list_del(&attr->list); + securityfs_remove(attr->dir); + ks_attr_put(attr); + } + mutex_unlock(&ks_lock); + return NOTIFY_DONE; +} + +static struct notifier_block ks_module_nb = { + .notifier_call = ks_module_notify, +}; + +/* ------------------------------------------------------------------ * + * Top-level securityfs files: control / engaged / taint * + * ------------------------------------------------------------------ */ + +static int ks_engaged_show(struct seq_file *m, void *v) +{ + struct ks_attr *attr; + + mutex_lock(&ks_lock); + list_for_each_entry(attr, &ks_engaged_list, list) { + seq_printf(m, "%s retval=%ld hits=%lu
... continue reading