From 69e9094e11c16853fd40e3b00ca25aa041ca8733 Mon Sep 17 00:00:00 2001 From: Dan Carpenter Date: Wed, 18 Apr 2018 14:33:15 +0300 Subject: [PATCH] spectre: warn about potential spectre vulnerabilities I'm not the world's leading expert in spectre (understatement), my understanding is that the issue is that the user supplied an invalid array offset and the computer reads from it speculatively, and only later does the check to say "Oh, this is an invalid offset, let's return -EINVAL;" But the problem is that traces of the read remain so we can use it to figure out what was in the invalid offset. The problem is that how vulnerable you are to Spectre depends on the CPU. Some CPUs speculatively execute up to 200 instructions in advance. Smatch has no concept of instructions. The kernel has an array_index_nospec() macro to turn off speculation for a specific variable. So what this check does is warn about where we're reading from an array and the offset is controlled by the user, and we haven't used the array_index_nospec() macro. There is a sort of lazy attempt to say, "If we've recently compared this offset to something, well that something was probably the array limit so it's probably under the 200 instruction barrier". I get hundreds of warnings, on yesterday's linux-next: 488 warn: potential spectre issue '' 258 warn: potential spectre issue '' (local cap) There are about 7 places which use the array_index_nospec() macro. Logically, you would have expected those numbers to be closer together so it probably indicates this test is very flawed. Anyway, this is a first draft... Signed-off-by: Dan Carpenter --- check_list.h | 2 + check_nospec.c | 116 ++++++++++++++++++++++++++++++++++++++++++++ check_spectre.c | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ smatch.h | 4 ++ 4 files changed, 270 insertions(+) create mode 100644 check_nospec.c create mode 100644 check_spectre.c diff --git a/check_list.h b/check_list.h index 85176742..7c9dfa80 100644 --- a/check_list.h +++ b/check_list.h @@ -175,6 +175,8 @@ CK(check_capable) CK(check_ns_capable) CK(check_test_bit) CK(check_dma_mapping_error) +CK(check_nospec) +CK(check_spectre) /* wine specific stuff */ CK(check_wine_filehandles) diff --git a/check_nospec.c b/check_nospec.c new file mode 100644 index 00000000..f322281b --- /dev/null +++ b/check_nospec.c @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 Oracle. + * + * 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, see http://www.gnu.org/copyleft/gpl.txt + */ + +#include +#include "parse.h" +#include "smatch.h" +#include "smatch_slist.h" +#include "smatch_extra.h" + +static int my_id; + +STATE(nospec); + +bool is_nospec(struct expression *expr) +{ + char *macro; + + if (get_state_expr(my_id, expr) == &nospec) + return true; + macro = get_macro_name(expr->pos); + if (macro && strcmp(macro, "array_index_nospec") == 0) + return true; + return false; +} + +static void nospec_assign(struct expression *expr) +{ + if (is_nospec(expr->right)) + set_state_expr(my_id, expr->left, &nospec); +} + +static void set_param_nospec(const char *name, struct symbol *sym, char *key, char *value) +{ + char fullname[256]; + + if (strcmp(key, "*$") == 0) + snprintf(fullname, sizeof(fullname), "*%s", name); + else if (strncmp(key, "$", 1) == 0) + snprintf(fullname, 256, "%s%s", name, key + 1); + else + return; + + set_state(my_id, fullname, sym, &nospec); +} + +static void match_call_info(struct expression *expr) +{ + struct expression *arg; + int i = 0; + + FOR_EACH_PTR(expr->args, arg) { + if (get_state_expr(my_id, arg) == &nospec) + sql_insert_caller_info(expr, NOSPEC, i, "$", ""); + i++; + } END_FOR_EACH_PTR(arg); +} + +static void struct_member_callback(struct expression *call, int param, char *printed_name, struct sm_state *sm) +{ + sql_insert_caller_info(call, NOSPEC, param, printed_name, ""); +} + +static void returned_struct_members(int return_id, char *return_ranges, struct expression *expr) +{ + struct symbol *returned_sym; + struct sm_state *sm; + const char *param_name; + int param; + + returned_sym = expr_to_sym(expr); + + FOR_EACH_MY_SM(my_id, __get_cur_stree(), sm) { + param = get_param_num_from_sym(sm->sym); + if (param < 0) { + if (!returned_sym || returned_sym != sm->sym) + continue; + param = -1; + } + + param_name = get_param_name(sm); + if (!param_name) + continue; + if (param != -1 && strcmp(param_name, "$") == 0) + continue; + + sql_insert_return_states(return_id, return_ranges, NOSPEC, param, param_name, ""); + } END_FOR_EACH_SM(sm); +} + +void check_nospec(int id) +{ + my_id = id; + + add_hook(&nospec_assign, ASSIGNMENT_HOOK); + + select_caller_info_hook(set_param_nospec, NOSPEC); + + + add_hook(&match_call_info, FUNCTION_CALL_HOOK); + add_member_info_callback(my_id, struct_member_callback); + add_split_return_callback(&returned_struct_members); +} diff --git a/check_spectre.c b/check_spectre.c new file mode 100644 index 00000000..e1a11676 --- /dev/null +++ b/check_spectre.c @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2018 Oracle. + * + * 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, see http://www.gnu.org/copyleft/gpl.txt + */ + +#include "smatch.h" +#include "smatch_extra.h" + +static int my_id; + +static int is_write(struct expression *expr) +{ + return 0; +} + +static int is_read(struct expression *expr) +{ + struct expression *parent, *last_parent; + struct statement *stmt; + + if (is_write(expr)) + return 0; + + last_parent = expr; + while ((parent = expr_get_parent_expr(expr))){ + + last_parent = parent; + + /* If we pass a value as a parameter that's a read, probably? */ +// if (parent->type == EXPR_CALL) +// return 1; + + if (parent->type == EXPR_ASSIGNMENT) { + if (parent->right == expr) + return 1; + if (parent->left == expr) + return 0; + } + expr = parent; + } + + stmt = expr_get_parent_stmt(last_parent); + if (stmt && stmt->type == STMT_RETURN) + return 1; + + return 0; +} + +static unsigned long long get_max_by_type(struct expression *expr) +{ + sval_t max = { + .type = &ullong_ctype, + .uvalue = -1ULL, + }; + struct symbol *type; + int cnt = 0; + + while (true) { + expr = strip_parens(expr); + type = get_type(expr); + if (type && sval_type_max(type).uvalue < max.uvalue) + max = sval_type_max(type); + if (expr->type == EXPR_PREOP) { + expr = expr->unop; + } else if (expr->type == EXPR_BINOP) { + if (expr->op == '%' || expr->op == '&') + expr = expr->right; + else + return max.uvalue; + } else { + expr = get_assigned_expr(expr); + if (!expr) + return max.uvalue; + } + if (cnt++ > 5) + return max.uvalue; + } + + return max.uvalue; +} + +static void array_check(struct expression *expr) +{ + struct expression_list *conditions; + struct expression *array_expr, *offset; + struct range_list *user_rl; +// struct bit_info *binfo; + int array_size; + char *name; + + expr = strip_expr(expr); + if (!is_array(expr)) + return; + + if (is_impossible_path()) + return; + if (is_write(expr)) + return; + if (!is_read(expr)) + return; + + array_expr = get_array_base(expr); + if (is_ignored_expr(my_id, array_expr)) + return; + + offset = get_array_offset(expr); + if (!get_user_rl(offset, &user_rl)) + return; + if (is_nospec(offset)) + return; + + array_size = get_array_size(array_expr); + if (array_size > 0 && get_max_by_type(offset) < array_size) + return; +// binfo = get_bit_info(offset); +// if (array_size > 0 && binfo && binfo->possible < array_size) +// return; + + conditions = get_conditions(offset); + + name = expr_to_str(array_expr); + sm_msg("warn: potential spectre issue '%s'%s", + name, conditions ? " (local cap)" : ""); + add_ignore_expr(my_id, array_expr); + free_string(name); +} + +void check_spectre(int id) +{ + my_id = id; + + if (option_project != PROJ_KERNEL) + return; + + add_hook(&array_check, OP_HOOK); +} diff --git a/smatch.h b/smatch.h index 27ee3491..0b1b94ab 100644 --- a/smatch.h +++ b/smatch.h @@ -735,6 +735,7 @@ enum info_type { CONSTRAINT = 1031, PASSES_TYPE = 1032, CONSTRAINT_REQUIRED = 1033, + NOSPEC = 1035, /* put random temporary stuff in the 7000-7999 range for testing */ USER_DATA3 = 8017, @@ -1063,6 +1064,9 @@ void update_mtag_data(struct expression *expr); extern int option_mem; unsigned long get_max_memory(void); +/* check_is_nospec.c */ +bool is_nospec(struct expression *expr); + static inline int type_bits(struct symbol *type) { if (!type) -- 2.11.4.GIT