mirror of
https://github.com/torvalds/linux
synced 2024-09-06 18:01:05 +00:00
![Marco Elver](/assets/img/avatar_default.png)
Numerous production kernel configs (see [1, 2]) are choosing to enable CONFIG_DEBUG_LIST, which is also being recommended by KSPP for hardened configs [3]. The motivation behind this is that the option can be used as a security hardening feature (e.g. CVE-2019-2215 and CVE-2019-2025 are mitigated by the option [4]). The feature has never been designed with performance in mind, yet common list manipulation is happening across hot paths all over the kernel. Introduce CONFIG_LIST_HARDENED, which performs list pointer checking inline, and only upon list corruption calls the reporting slow path. To generate optimal machine code with CONFIG_LIST_HARDENED: 1. Elide checking for pointer values which upon dereference would result in an immediate access fault (i.e. minimal hardening checks). The trade-off is lower-quality error reports. 2. Use the __preserve_most function attribute (available with Clang, but not yet with GCC) to minimize the code footprint for calling the reporting slow path. As a result, function size of callers is reduced by avoiding saving registers before calling the rarely called reporting slow path. Note that all TUs in lib/Makefile already disable function tracing, including list_debug.c, and __preserve_most's implied notrace has no effect in this case. 3. Because the inline checks are a subset of the full set of checks in __list_*_valid_or_report(), always return false if the inline checks failed. This avoids redundant compare and conditional branch right after return from the slow path. As a side-effect of the checks being inline, if the compiler can prove some condition to always be true, it can completely elide some checks. Since DEBUG_LIST is functionally a superset of LIST_HARDENED, the Kconfig variables are changed to reflect that: DEBUG_LIST selects LIST_HARDENED, whereas LIST_HARDENED itself has no dependency on DEBUG_LIST. Running netperf with CONFIG_LIST_HARDENED (using a Clang compiler with "preserve_most") shows throughput improvements, in my case of ~7% on average (up to 20-30% on some test cases). Link: https://r.android.com/1266735 [1] Link: https://gitlab.archlinux.org/archlinux/packaging/packages/linux/-/blob/main/config [2] Link: https://kernsec.org/wiki/index.php/Kernel_Self_Protection_Project/Recommended_Settings [3] Link: https://googleprojectzero.blogspot.com/2019/11/bad-binder-android-in-wild-exploit.html [4] Signed-off-by: Marco Elver <elver@google.com> Link: https://lore.kernel.org/r/20230811151847.1594958-3-elver@google.com Signed-off-by: Kees Cook <keescook@chromium.org>
73 lines
2.3 KiB
C
73 lines
2.3 KiB
C
/*
|
|
* Copyright 2006, Red Hat, Inc., Dave Jones
|
|
* Released under the General Public License (GPL).
|
|
*
|
|
* This file contains the linked list validation and error reporting for
|
|
* LIST_HARDENED and DEBUG_LIST.
|
|
*/
|
|
|
|
#include <linux/export.h>
|
|
#include <linux/list.h>
|
|
#include <linux/bug.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/rculist.h>
|
|
|
|
/*
|
|
* Check that the data structures for the list manipulations are reasonably
|
|
* valid. Failures here indicate memory corruption (and possibly an exploit
|
|
* attempt).
|
|
*/
|
|
|
|
__list_valid_slowpath
|
|
bool __list_add_valid_or_report(struct list_head *new, struct list_head *prev,
|
|
struct list_head *next)
|
|
{
|
|
if (CHECK_DATA_CORRUPTION(prev == NULL,
|
|
"list_add corruption. prev is NULL.\n") ||
|
|
CHECK_DATA_CORRUPTION(next == NULL,
|
|
"list_add corruption. next is NULL.\n") ||
|
|
CHECK_DATA_CORRUPTION(next->prev != prev,
|
|
"list_add corruption. next->prev should be prev (%px), but was %px. (next=%px).\n",
|
|
prev, next->prev, next) ||
|
|
CHECK_DATA_CORRUPTION(prev->next != next,
|
|
"list_add corruption. prev->next should be next (%px), but was %px. (prev=%px).\n",
|
|
next, prev->next, prev) ||
|
|
CHECK_DATA_CORRUPTION(new == prev || new == next,
|
|
"list_add double add: new=%px, prev=%px, next=%px.\n",
|
|
new, prev, next))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
EXPORT_SYMBOL(__list_add_valid_or_report);
|
|
|
|
__list_valid_slowpath
|
|
bool __list_del_entry_valid_or_report(struct list_head *entry)
|
|
{
|
|
struct list_head *prev, *next;
|
|
|
|
prev = entry->prev;
|
|
next = entry->next;
|
|
|
|
if (CHECK_DATA_CORRUPTION(next == NULL,
|
|
"list_del corruption, %px->next is NULL\n", entry) ||
|
|
CHECK_DATA_CORRUPTION(prev == NULL,
|
|
"list_del corruption, %px->prev is NULL\n", entry) ||
|
|
CHECK_DATA_CORRUPTION(next == LIST_POISON1,
|
|
"list_del corruption, %px->next is LIST_POISON1 (%px)\n",
|
|
entry, LIST_POISON1) ||
|
|
CHECK_DATA_CORRUPTION(prev == LIST_POISON2,
|
|
"list_del corruption, %px->prev is LIST_POISON2 (%px)\n",
|
|
entry, LIST_POISON2) ||
|
|
CHECK_DATA_CORRUPTION(prev->next != entry,
|
|
"list_del corruption. prev->next should be %px, but was %px. (prev=%px)\n",
|
|
entry, prev->next, prev) ||
|
|
CHECK_DATA_CORRUPTION(next->prev != entry,
|
|
"list_del corruption. next->prev should be %px, but was %px. (next=%px)\n",
|
|
entry, next->prev, next))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
EXPORT_SYMBOL(__list_del_entry_valid_or_report);
|