Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 28facdf7 authored by Jann Horn's avatar Jann Horn Committed by Greg Kroah-Hartman
Browse files

exit: Put an upper limit on how often we can oops



commit d4ccd54d28d3c8598e2354acc13e28c060961dbb upstream.

Many Linux systems are configured to not panic on oops; but allowing an
attacker to oops the system **really** often can make even bugs that look
completely unexploitable exploitable (like NULL dereferences and such) if
each crash elevates a refcount by one or a lock is taken in read mode, and
this causes a counter to eventually overflow.

The most interesting counters for this are 32 bits wide (like open-coded
refcounts that don't use refcount_t). (The ldsem reader count on 32-bit
platforms is just 16 bits, but probably nobody cares about 32-bit platforms
that much nowadays.)

So let's panic the system if the kernel is constantly oopsing.

The speed of oopsing 2^32 times probably depends on several factors, like
how long the stack trace is and which unwinder you're using; an empirically
important one is whether your console is showing a graphical environment or
a text console that oopses will be printed to.
In a quick single-threaded benchmark, it looks like oopsing in a vfork()
child with a very short stack trace only takes ~510 microseconds per run
when a graphical console is active; but switching to a text console that
oopses are printed to slows it down around 87x, to ~45 milliseconds per
run.
(Adding more threads makes this faster, but the actual oops printing
happens under &die_lock on x86, so you can maybe speed this up by a factor
of around 2 and then any further improvement gets eaten up by lock
contention.)

It looks like it would take around 8-12 days to overflow a 32-bit counter
with repeated oopsing on a multi-core X86 system running a graphical
environment; both me (in an X86 VM) and Seth (with a distro kernel on
normal hardware in a standard configuration) got numbers in that ballpark.

12 days aren't *that* short on a desktop system, and you'd likely need much
longer on a typical server system (assuming that people don't run graphical
desktop environments on their servers), and this is a *very* noisy and
violent approach to exploiting the kernel; and it also seems to take orders
of magnitude longer on some machines, probably because stuff like EFI
pstore will slow it down a ton if that's active.

Signed-off-by: default avatarJann Horn <jannh@google.com>
Link: https://lore.kernel.org/r/20221107201317.324457-1-jannh@google.com


Reviewed-by: default avatarLuis Chamberlain <mcgrof@kernel.org>
Signed-off-by: default avatarKees Cook <keescook@chromium.org>
Link: https://lore.kernel.org/r/20221117234328.594699-2-keescook@chromium.org


Signed-off-by: default avatarEric Biggers <ebiggers@google.com>
Signed-off-by: default avatarSasha Levin <sashal@kernel.org>
parent 3fa431ba
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -557,6 +557,14 @@ numa_balancing_scan_size_mb is how many megabytes worth of pages are
scanned for a given scan.


oops_limit
==========

Number of kernel oopses after which the kernel should panic when
``panic_on_oops`` is not set. Setting this to 0 or 1 has the same effect
as setting ``panic_on_oops=1``.


osrelease, ostype & version:
============================

+43 −0
Original line number Diff line number Diff line
@@ -69,6 +69,33 @@
#include <asm/pgtable.h>
#include <asm/mmu_context.h>

/*
 * The default value should be high enough to not crash a system that randomly
 * crashes its kernel from time to time, but low enough to at least not permit
 * overflowing 32-bit refcounts or the ldsem writer count.
 */
static unsigned int oops_limit = 10000;

#ifdef CONFIG_SYSCTL
static struct ctl_table kern_exit_table[] = {
	{
		.procname       = "oops_limit",
		.data           = &oops_limit,
		.maxlen         = sizeof(oops_limit),
		.mode           = 0644,
		.proc_handler   = proc_douintvec,
	},
	{ }
};

static __init int kernel_exit_sysctls_init(void)
{
	register_sysctl_init("kernel", kern_exit_table);
	return 0;
}
late_initcall(kernel_exit_sysctls_init);
#endif

static void __unhash_process(struct task_struct *p, bool group_dead)
{
	nr_threads--;
@@ -866,10 +893,26 @@ EXPORT_SYMBOL_GPL(do_exit);

void __noreturn make_task_dead(int signr)
{
	static atomic_t oops_count = ATOMIC_INIT(0);

	/*
	 * Take the task off the cpu after something catastrophic has
	 * happened.
	 */

	/*
	 * Every time the system oopses, if the oops happens while a reference
	 * to an object was held, the reference leaks.
	 * If the oops doesn't also leak memory, repeated oopsing can cause
	 * reference counters to wrap around (if they're not using refcount_t).
	 * This means that repeated oopsing can make unexploitable-looking bugs
	 * exploitable through repeated oopsing.
	 * To make sure this can't happen, place an upper bound on how often the
	 * kernel may oops without panic().
	 */
	if (atomic_inc_return(&oops_count) >= READ_ONCE(oops_limit))
		panic("Oopsed too often (kernel.oops_limit is %d)", oops_limit);

	do_exit(signr);
}