This adds a new -mmitigate-rop option to the i386 port. The idea is to
mitigate against certain forms of attack called "return oriented
programming" that some of our security folks are concerned about. The
basic idea is that the stack gets smashed and then, just by chaining
function returns and some preceding instructions, you can have a
Turing-complete program to perform an attack. The function returns can
be either normal, intended ones that are part of the program, or parts
of the instruction encoding of other sequences.
This patch is a small step towards preventing this kind of attack. I
have a few more steps queued (not quite ready for stage 1), but
additional work will be necessary to give reasonable protection. Here,
I'm only concerned with modr/m bytes, and avoiding certain specific
opcodes that encode a "return" instruction. Two strategies are
available: rename entire chains of registers, or insert extra reg-reg
copies if there is a free scratch register.
The modrm byte computation is not a full one, it is only intended to be
able to tell whether a value is risky or not.
This was bootstrapped and tested on x86_64-linux. I thought I'd also
done a full test with -mmitigate-rop forced always on, but a typo
thwarted that. An earlier set of test results looked reasonable but I
did not have a baseline to compare against, so I'll be retesting this.
Bernd
* regrename.h (struct du_head): Add target_data_1 and target_data_2
fields.
* regrename.c (create_new_chain): Clear entire struct after allocating.
* config/i386/i386.opt (mmitigate-rop): New option.
* doc/invoke.texi (mmitigate-rop): Document.
* config/i386/i386.c: Include "regrename.h".
(ix86_rop_should_change_byte_p, reg_encoded_number,
ix86_get_modrm_for_rop, set_rop_modrm_reg_bits, ix86_mitigate_rop): New
static functions.
(ix86_reorg): Call ix86_mitigate_rop if -fmitigate-rop.
* config/i386/i386.md (attr "modrm_class"): New.
(cmp<mode>_ccno_1, mov<mode>_xor, movstrict<mode>_xor,
x86_mov<mode>cc_0_m1. x86_mov<mode>cc_0_m1_se,
x86_mov<mode>cc_0_m1_neg): Override modrm_class attribute.
diff --git a/gcc/config/i386/i386.c b/gcc/config/i386/i386.c
index 38953dd..b989fc1 100644
--- a/gcc/config/i386/i386.c
+++ b/gcc/config/i386/i386.c
@@ -88,6 +88,7 @@ along with GCC; see the file COPYING3. If not see
#include "tree-chkp.h"
#include "rtl-chkp.h"
#include "dbgcnt.h"
+#include "regrename.h"
/* This file should be included last. */
#include "target-def.h"
@@ -3987,6 +3988,15 @@ ix86_debug_options (void)
return;
}
+/* Return true if T is one of the bytes we should avoid with
+ -fmitigate-rop. */
+
+static bool
+ix86_rop_should_change_byte_p (int t)
+{
+ return t == 0xc2 || t == 0xc3 || t == 0xca || t == 0xcb;
+}
+
static const char *stringop_alg_names[] = {
#define DEF_ENUM
#define DEF_ALG(alg, name) #name,
@@ -26965,6 +26975,100 @@ ix86_instantiate_decls (void)
instantiate_decl_rtl (s->rtl);
}
+/* Return the number used for encoding REG, in the range 0..7. */
+
+static int
+reg_encoded_number (rtx reg)
+{
+ unsigned regno = REGNO (reg);
+ switch (regno)
+ {
+ case AX_REG:
+ return 0;
+ case CX_REG:
+ return 1;
+ case DX_REG:
+ return 2;
+ case BX_REG:
+ return 3;
+ case SP_REG:
+ return 4;
+ case BP_REG:
+ return 5;
+ case SI_REG:
+ return 6;
+ case DI_REG:
+ return 7;
+ default:
+ break;
+ }
+ if (IN_RANGE (regno, FIRST_STACK_REG, LAST_STACK_REG))
+ return regno - FIRST_STACK_REG;
+ if (IN_RANGE (regno, FIRST_SSE_REG, LAST_SSE_REG))
+ return regno - FIRST_SSE_REG;
+ if (IN_RANGE (regno, FIRST_MMX_REG, LAST_MMX_REG))
+ return regno - FIRST_MMX_REG;
+ if (IN_RANGE (regno, FIRST_REX_SSE_REG, LAST_REX_SSE_REG))
+ return regno - FIRST_REX_SSE_REG;
+ if (IN_RANGE (regno, FIRST_REX_INT_REG, LAST_REX_INT_REG))
+ return regno - FIRST_REX_INT_REG;
+ if (IN_RANGE (regno, FIRST_MASK_REG, LAST_MASK_REG))
+ return regno - FIRST_MASK_REG;
+ if (IN_RANGE (regno, FIRST_BND_REG, LAST_BND_REG))
+ return regno - FIRST_BND_REG;
+ return -1;
+}
+
+/* Given an insn INSN with NOPERANDS OPERANDS, return the modr/m byte used
+ in its encoding if it could be relevant for ROP mitigation, otherwise
+ return -1. If POPNO0 and POPNO1 are nonnull, store the operand numbers
+ used for calculating it into them. */
+
+static int
+ix86_get_modrm_for_rop (rtx_insn *insn, rtx *operands, int noperands,
+ int *popno0 = 0, int *popno1 = 0)
+{
+ if (asm_noperands (PATTERN (insn)) >= 0)
+ return -1;
+ int has_modrm = get_attr_modrm (insn);
+ if (!has_modrm)
+ return -1;
+ enum attr_modrm_class cls = get_attr_modrm_class (insn);
+ rtx op0, op1;
+ switch (cls)
+ {
+ case MODRM_CLASS_OP02:
+ gcc_assert (noperands >= 3);
+ if (popno0)
+ {
+ *popno0 = 0;
+ *popno1 = 2;
+ }
+ op0 = operands[0];
+ op1 = operands[2];
+ break;
+ case MODRM_CLASS_OP01:
+ gcc_assert (noperands >= 2);
+ if (popno0)
+ {
+ *popno0 = 0;
+ *popno1 = 1;
+ }
+ op0 = operands[0];
+ op1 = operands[1];
+ break;
+ default:
+ return -1;
+ }
+ if (REG_P (op0) && REG_P (op1))
+ {
+ int enc0 = reg_encoded_number (op0);
+ int enc1 = reg_encoded_number (op1);
+ return 0xc0 + (enc1 << 3) + enc0;
+ }
+ return -1;
+}
+
/* Check whether x86 address PARTS is a pc-relative address. */
static bool
@@ -44918,6 +45034,215 @@ ix86_seh_fixup_eh_fallthru (void)
}
}
+/* Given a register number BASE, the lowest of a group of registers, update
+ regsets IN and OUT with the registers that should be avoided in input
+ and output operands respectively when trying to avoid generating a modr/m
+ byte for -fmitigate-rop. */
+
+static void
+set_rop_modrm_reg_bits (int base, HARD_REG_SET &in, HARD_REG_SET &out)
+{
+ SET_HARD_REG_BIT (out, base);
+ SET_HARD_REG_BIT (out, base + 1);
+ SET_HARD_REG_BIT (in, base + 2);
+ SET_HARD_REG_BIT (in, base + 3);
+}
+
+/* Called if -fmitigate_rop is in effect. Try to rewrite instructions so
+ that certain encodings of modr/m bytes do not occur. */
+static void
+ix86_mitigate_rop (void)
+{
+ HARD_REG_SET input_risky;
+ HARD_REG_SET output_risky;
+ HARD_REG_SET inout_risky;
+
+ CLEAR_HARD_REG_SET (output_risky);
+ CLEAR_HARD_REG_SET (input_risky);
+ SET_HARD_REG_BIT (output_risky, AX_REG);
+ SET_HARD_REG_BIT (output_risky, CX_REG);
+ SET_HARD_REG_BIT (input_risky, BX_REG);
+ SET_HARD_REG_BIT (input_risky, DX_REG);
+ set_rop_modrm_reg_bits (FIRST_SSE_REG, input_risky, output_risky);
+ set_rop_modrm_reg_bits (FIRST_REX_INT_REG, input_risky, output_risky);
+ set_rop_modrm_reg_bits (FIRST_REX_SSE_REG, input_risky, output_risky);
+ set_rop_modrm_reg_bits (FIRST_EXT_REX_SSE_REG, input_risky, output_risky);
+ set_rop_modrm_reg_bits (FIRST_MASK_REG, input_risky, output_risky);
+ set_rop_modrm_reg_bits (FIRST_BND_REG, input_risky, output_risky);
+ COPY_HARD_REG_SET (inout_risky, input_risky);
+ IOR_HARD_REG_SET (inout_risky, output_risky);
+
+ compute_bb_for_insn ();
+ df_note_add_problem ();
+ df_analyze ();
+
+ regrename_init (true);
+ regrename_analyze (NULL);
+
+ auto_vec<du_head_p> cands;
+
+ for (rtx_insn *insn = get_insns (); insn; insn = NEXT_INSN (insn))
+ {
+ if (!NONDEBUG_INSN_P (insn))
+ continue;
+
+ if (GET_CODE (PATTERN (insn)) == USE
+ || GET_CODE (PATTERN (insn)) == CLOBBER)
+ continue;
+
+ extract_insn (insn);
+
+ int opno0, opno1;
+ int modrm = ix86_get_modrm_for_rop (insn, recog_data.operand,
+ recog_data.n_operands, &opno0,
+ &opno1);
+
+ if (!ix86_rop_should_change_byte_p (modrm))
+ continue;
+
+ insn_rr_info *info = &insn_rr[INSN_UID (insn)];
+
+ /* This happens when regrename has to fail a block. */
+ if (!info->op_info)
+ continue;
+
+ if (info->op_info[opno0].n_chains != 0)
+ {
+ gcc_assert (info->op_info[opno0].n_chains == 1);
+ du_head_p op0c;
+ op0c = regrename_chain_from_id (info->op_info[opno0].heads[0]->id);
+ if (op0c->target_data_1 + op0c->target_data_2 == 0
+ && !op0c->cannot_rename)
+ cands.safe_push (op0c);
+
+ op0c->target_data_1++;
+ }
+ if (info->op_info[opno1].n_chains != 0)
+ {
+ gcc_assert (info->op_info[opno1].n_chains == 1);
+ du_head_p op1c;
+ op1c = regrename_chain_from_id (info->op_info[opno1].heads[0]->id);
+ if (op1c->target_data_1 + op1c->target_data_2 == 0
+ && !op1c->cannot_rename)
+ cands.safe_push (op1c);
+
+ op1c->target_data_2++;
+ }
+ }
+
+ int i;
+ du_head_p head;
+ FOR_EACH_VEC_ELT (cands, i, head)
+ {
+ int old_reg, best_reg;
+ HARD_REG_SET unavailable;
+
+ CLEAR_HARD_REG_SET (unavailable);
+ if (head->target_data_1)
+ IOR_HARD_REG_SET (unavailable, output_risky);
+ if (head->target_data_2)
+ IOR_HARD_REG_SET (unavailable, input_risky);
+
+ int n_uses;
+ reg_class superclass = regrename_find_superclass (head, &n_uses,
+ &unavailable);
+ old_reg = head->regno;
+ best_reg = find_rename_reg (head, superclass, &unavailable,
+ old_reg, false);
+ bool ok = regrename_do_replace (head, best_reg);
+ gcc_assert (ok);
+ if (dump_file)
+ fprintf (dump_file, "Chain %d renamed as %s in %s\n", head->id,
+ reg_names[best_reg], reg_class_names[superclass]);
+
+ }
+
+ regrename_finish ();
+
+ df_analyze ();
+
+ basic_block bb;
+ regset_head live;
+
+ INIT_REG_SET (&live);
+
+ FOR_EACH_BB_FN (bb, cfun)
+ {
+ rtx_insn *insn;
+
+ COPY_REG_SET (&live, DF_LR_OUT (bb));
+ df_simulate_initialize_backwards (bb, &live);
+
+ FOR_BB_INSNS_REVERSE (bb, insn)
+ {
+ if (!NONDEBUG_INSN_P (insn))
+ continue;
+
+ df_simulate_one_insn_backwards (bb, insn, &live);
+
+ if (GET_CODE (PATTERN (insn)) == USE
+ || GET_CODE (PATTERN (insn)) == CLOBBER)
+ continue;
+
+ extract_insn (insn);
+ constrain_operands_cached (insn, reload_completed);
+ int opno0, opno1;
+ int modrm = ix86_get_modrm_for_rop (insn, recog_data.operand,
+ recog_data.n_operands, &opno0,
+ &opno1);
+ if (modrm < 0
+ || !ix86_rop_should_change_byte_p (modrm)
+ || opno0 == opno1)
+ continue;
+
+ rtx oldreg = recog_data.operand[opno1];
+ preprocess_constraints (insn);
+ const operand_alternative *alt = which_op_alt ();
+
+ int i;
+ for (i = 0; i < recog_data.n_operands; i++)
+ if (i != opno1
+ && alt[i].earlyclobber
+ && reg_overlap_mentioned_p (recog_data.operand[i],
+ oldreg))
+ break;
+
+ if (i < recog_data.n_operands)
+ continue;
+
+ if (dump_file)
+ fprintf (dump_file,
+ "attempting to fix modrm byte in insn %d:"
+ " reg %d class %s", INSN_UID (insn), REGNO (oldreg),
+ reg_class_names[alt[opno1].cl]);
+
+ HARD_REG_SET unavailable;
+ REG_SET_TO_HARD_REG_SET (unavailable, &live);
+ SET_HARD_REG_BIT (unavailable, REGNO (oldreg));
+ IOR_COMPL_HARD_REG_SET (unavailable, call_used_reg_set);
+ IOR_HARD_REG_SET (unavailable, fixed_reg_set);
+ IOR_HARD_REG_SET (unavailable, output_risky);
+ IOR_COMPL_HARD_REG_SET (unavailable,
+ reg_class_contents[alt[opno1].cl]);
+
+ for (i = 0; i < FIRST_PSEUDO_REGISTER; i++)
+ if (!TEST_HARD_REG_BIT (unavailable, i))
+ break;
+ if (i == FIRST_PSEUDO_REGISTER)
+ {
+ if (dump_file)
+ fprintf (dump_file, ", none available\n");
+ continue;
+ }
+ if (dump_file)
+ fprintf (dump_file, " -> %d\n", i);
+ rtx newreg = gen_rtx_REG (recog_data.operand_mode[opno1], i);
+ validate_change (insn, recog_data.operand_loc[opno1], newreg, false);
+ insn = emit_insn_before (gen_move_insn (newreg, oldreg), insn);
+ }
+ }
+}
+
/* Implement machine specific optimizations. We implement padding of returns
for K8 CPUs and pass to avoid 4 jumps in the single 16 byte window. */
static void
@@ -44927,6 +45252,9 @@ ix86_reorg (void)
with old MDEP_REORGS that are not CFG based. Recompute it now. */
compute_bb_for_insn ();
+ if (flag_mitigate_rop)
+ ix86_mitigate_rop ();
+
if (TARGET_SEH && current_function_has_exception_handlers ())
ix86_seh_fixup_eh_fallthru ();
diff --git a/gcc/config/i386/i386.md b/gcc/config/i386/i386.md
index d0c0d23..b2ff1af 100644
--- a/gcc/config/i386/i386.md
+++ b/gcc/config/i386/i386.md
@@ -637,6 +637,19 @@
]
(const_int 1)))
+(define_attr "modrm_class" "none,incdec,op0,op01,op02,pushpop,unknown"
+ (cond [(eq_attr "modrm" "0")
+ (const_string "none")
+ (eq_attr "type" "alu,imul,ishift")
+ (const_string "op02")
+ (eq_attr "type" "imov,imovx,lea,alu1,icmp")
+ (const_string "op01")
+ (eq_attr "type" "incdec")
+ (const_string "incdec")
+ (eq_attr "type" "push,pop")
+ (const_string "pushpop")]
+ (const_string "unknown")))
+
;; The (bounding maximum) length of an instruction in bytes.
;; ??? fistp and frndint are in fact fldcw/{fistp,frndint}/fldcw sequences.
;; Later we may want to split them and compute proper length as for
@@ -1235,6 +1248,7 @@
cmp{<imodesuffix>}\t{%1, %0|%0, %1}"
[(set_attr "type" "test,icmp")
(set_attr "length_immediate" "0,1")
+ (set_attr "modrm_class" "op0,unknown")
(set_attr "mode" "<MODE>")])
(define_insn "*cmp<mode>_1"
@@ -1942,6 +1956,7 @@
"reload_completed"
"xor{l}\t%k0, %k0"
[(set_attr "type" "alu1")
+ (set_attr "modrm_class" "op0")
(set_attr "mode" "SI")
(set_attr "length_immediate" "0")])
@@ -2693,6 +2708,7 @@
"reload_completed"
"xor{<imodesuffix>}\t%0, %0"
[(set_attr "type" "alu1")
+ (set_attr "modrm_class" "op0")
(set_attr "mode" "<MODE>")
(set_attr "length_immediate" "0")])
@@ -16802,6 +16818,7 @@
; Since we don't have the proper number of operands for an alu insn,
; fill in all the blanks.
[(set_attr "type" "alu")
+ (set_attr "modrm_class" "op0")
(set_attr "use_carry" "1")
(set_attr "pent_pair" "pu")
(set_attr "memory" "none")
@@ -16819,6 +16836,7 @@
""
"sbb{<imodesuffix>}\t%0, %0"
[(set_attr "type" "alu")
+ (set_attr "modrm_class" "op0")
(set_attr "use_carry" "1")
(set_attr "pent_pair" "pu")
(set_attr "memory" "none")
@@ -16834,6 +16852,7 @@
""
"sbb{<imodesuffix>}\t%0, %0"
[(set_attr "type" "alu")
+ (set_attr "modrm_class" "op0")
(set_attr "use_carry" "1")
(set_attr "pent_pair" "pu")
(set_attr "memory" "none")
diff --git a/gcc/config/i386/i386.opt b/gcc/config/i386/i386.opt
index 687ae0c..7335ec8 100644
--- a/gcc/config/i386/i386.opt
+++ b/gcc/config/i386/i386.opt
@@ -889,3 +889,7 @@ Enum(stack_protector_guard) String(tls) Value(SSP_TLS)
EnumValue
Enum(stack_protector_guard) String(global) Value(SSP_GLOBAL)
+
+mmitigate-rop
+Target Var(flag_mitigate_rop) Init(0)
+Attempt to avoid generating instruction sequences containing ret bytes
diff --git a/gcc/regrename.c b/gcc/regrename.c
index c328c1b..788c8fd 100644
--- a/gcc/regrename.c
+++ b/gcc/regrename.c
@@ -223,11 +231,10 @@ create_new_chain (unsigned this_regno, unsigned this_nregs, rtx *loc,
struct du_chain *this_du;
int nregs;
+ memset (head, 0, sizeof *head);
head->next_chain = open_chains;
head->regno = this_regno;
head->nregs = this_nregs;
- head->need_caller_save_reg = 0;
- head->cannot_rename = 0;
id_to_chain.safe_push (head);
head->id = current_id++;
diff --git a/gcc/regrename.h b/gcc/regrename.h
index bbe156d..f9f9481 100644
--- a/gcc/regrename.h
+++ b/gcc/regrename.h
@@ -45,6 +45,9 @@ struct du_head
such as the SET_DEST of a CALL_INSN or an asm operand that used
to be a hard register. */
unsigned int cannot_rename:1;
+
+ unsigned int target_data_1;
+ unsigned int target_data_2;
};
typedef struct du_head *du_head_p;
diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
index 0121832..c68a6cb 100644
--- a/gcc/doc/invoke.texi
+++ b/gcc/doc/invoke.texi
@@ -1104,7 +1104,8 @@ See RS/6000 and PowerPC Options.
-m32 -m64 -mx32 -m16 -miamcu -mlarge-data-threshold=@var{num} @gol
-msse2avx -mfentry -mrecord-mcount -mnop-mcount -m8bit-idiv @gol
-mavx256-split-unaligned-load -mavx256-split-unaligned-store @gol
--malign-data=@var{type} -mstack-protector-guard=@var{guard}}
+-malign-data=@var{type} -mstack-protector-guard=@var{guard} @gol
+-mmitigate-rop}
@emph{x86 Windows Options}
@gccoptlist{-mconsole -mcygwin -mno-cygwin -mdll @gol
@@ -23337,6 +23338,11 @@ locations are @samp{global} for global canary or @samp{tls} for per-thread
canary in the TLS block (the default). This option has effect only when
@option{-fstack-protector} or @option{-fstack-protector-all} is specified.
+@item -mmitigate-rop
+@opindex mmitigate-rop
+Try to avoid generating code sequences that contain unintended return
+opcodes, to mitigate against certain forms of attack.
+
@end table
These @samp{-m} switches are supported in addition to the above