From: Richard Sandiford <richard.sandiford@arm.com>
To: Indu Bhagat <indu.bhagat@oracle.com>
Cc: binutils@sourceware.org, Richard.Earnshaw@arm.com
Subject: Re: [PATCH,V4 7/8] gas: aarch64: add experimental support for SCFI
Date: Thu, 11 Jul 2024 14:15:26 +0100 [thread overview]
Message-ID: <mptr0c0ul1t.fsf@arm.com> (raw)
In-Reply-To: <f123776f-3913-4345-aa9b-5ece6a05f87e@oracle.com> (Indu Bhagat's message of "Wed, 10 Jul 2024 23:30:45 -0700")
Indu Bhagat <indu.bhagat@oracle.com> writes:
> On 7/1/24 12:49, Richard Sandiford wrote:
>> Indu Bhagat <indu.bhagat@oracle.com> writes:
>>> +{
>>> + enum aarch64_operand_class opnd_class;
>>> + unsigned int dw2reg_num = 0;
>>> +
>>> + opnd_class = aarch64_get_operand_class (opnd->type);
>>> +
>>> + switch (opnd_class)
>>> + {
>>> + case AARCH64_OPND_CLASS_FP_REG:
>>> + dw2reg_num = opnd->reg.regno + 64;
>>> + break;
>>> + case AARCH64_OPND_CLASS_SVE_REGLIST:
>>> + dw2reg_num = opnd->reglist.first_regno + 64;
>>> + break;
>>> + case AARCH64_OPND_CLASS_MODIFIED_REG:
>>> + case AARCH64_OPND_CLASS_INT_REG:
>>> + case AARCH64_OPND_CLASS_ADDRESS:
>>> + /* Use a dummy register value in case of WZR, else this will be an
>>> + incorrect dependency on REG_SP. */
>>> + if (!sp_allowed_p && opnd->reg.regno == REG_SP)
>>> + dw2reg_num = GINSN_DW2_REGNUM_R1_DUMMY;
>>> + else
>>> + /* For GPRs of our interest (callee-saved regs, SP, FP, LR),
>>> + DWARF register number is the same as AArch64 register number. */
>>> + dw2reg_num = opnd->reg.regno;
>>> + break;
>>
>> I think the AARCH64_OPND_CLASS_ADDRESS case should look at opnd->addr
>> instead. AARCH64_OPND_CLASS_MODIFIED_REG should look at opnd->shifter.
>>
>
> Made the correction for AARCH64_OPND_CLASS_ADDRESS.
>
> But for AARCH64_OPND_CLASS_MODIFIED_REG, the register information is
> still correct in opnd->reg.regno, IIUC. It seems to me that the only
> information available in the opnd->shifter is how the register is
> modified by additional work (shift, multiply etc.); This information is
> not used by SCFI:
> - an add/sub with two source register and destination REG_SP/ REG_FP
> makes REG_SP/ REG_FP untraceable. So ignoring the shift amount etc does
> not hurt SCFI correctness.
> - Cant think of other operations where the shift amount will affect
> SCFI correctness..
>
> So I am not sure of the "AARCH64_OPND_CLASS_MODIFIED_REG should look at
> opnd->shifter." of the review comment.
It's more about type correctness. "shifter" is the data associated with
AARCH64_OPND_CLASS_MODIFIED_REG and "reg" is the data associated with
AARCH64_OPND_CLASS_INT_REG etc. I think it's mostly a coincidence
that the "reg" and "shifter" alternatives of the union put the register
at the same byte offset from the start of the structure.
>>> + default:
>>> + as_bad ("Unexpected value in ginsn_dw2_regnum");
>>> + break;
>>> + }
>>> +
>>> + return dw2reg_num;
>>> +}
>>> +
>>> +/* Generate ginsn for addsub instructions with immediate opnd. */
>>> +
>>> +static ginsnS *
>>> +aarch64_ginsn_addsub_imm (const symbolS *insn_end_sym)
>>> +{
>>> + ginsnS *ginsn = NULL;
>>> + bool add_p, sub_p;
>>> + offsetT src_imm = 0;
>>> + unsigned int dst_reg, opnd_reg;
>>> + aarch64_opnd_info *dst, *opnd;
>>> + ginsnS *(*ginsn_func) (const symbolS *, bool,
>>> + enum ginsn_src_type, unsigned int, offsetT,
>>> + enum ginsn_src_type, unsigned int, offsetT,
>>> + enum ginsn_dst_type, unsigned int, offsetT);
>>> +
>>> + aarch64_inst *base = &inst.base;
>>> + const aarch64_opcode *opcode = base->opcode;
>>> +
>>> + add_p = aarch64_opcode_subclass_p (opcode, F_ARITH_ADD);
>>> + sub_p = aarch64_opcode_subclass_p (opcode, F_ARITH_SUB);
>>> + gas_assert (add_p || sub_p);
>>> + ginsn_func = add_p ? ginsn_new_add : ginsn_new_sub;
>>> +
>>> + gas_assert (aarch64_num_of_operands (opcode) == 3);
>>> + dst = &base->operands[0];
>>> + opnd = &base->operands[1];
>>> +
>>> + dst_reg = ginsn_dw2_regnum (dst, true);
>>> +
>>> + if (aarch64_gas_internal_fixup_p () && inst.reloc.exp.X_op == O_constant)
>>> + src_imm = inst.reloc.exp.X_add_number;
>>> + /* For any other relocation type, e.g., in add reg, reg, symbol, skip now
>>> + and handle via aarch64_ginsn_unhandled () code path. */
>>> + else if (inst.reloc.type != BFD_RELOC_UNUSED)
>>> + return ginsn;
>>> + /* FIXME - verify the understanding and remove assert. */
>>> + else
>>> + gas_assert (0);
>>> +
>>> + opnd_reg = ginsn_dw2_regnum (opnd, true);
>>> +
>>> + ginsn = ginsn_func (insn_end_sym, true,
>>> + GINSN_SRC_REG, opnd_reg, 0,
>>> + GINSN_SRC_IMM, 0, src_imm,
>>> + GINSN_DST_REG, dst_reg, 0);
>>> + ginsn_set_where (ginsn);
>>> +
>>> + return ginsn;
>>> +}
>>> +
>>> +/* Generate ginsn for addsub instructions with reg opnd. */
>>> +
>>> +static ginsnS *
>>> +aarch64_ginsn_addsub_reg (const symbolS *insn_end_sym)
>>> +{
>>> + ginsnS *ginsn = NULL;
>>> + bool add_p, sub_p;
>>> + unsigned int dst_reg, src1_reg, src2_reg;
>>> + aarch64_opnd_info *dst, *src1, *src2;
>>> + ginsnS *(*ginsn_func) (const symbolS *, bool,
>>> + enum ginsn_src_type, unsigned int, offsetT,
>>> + enum ginsn_src_type, unsigned int, offsetT,
>>> + enum ginsn_dst_type, unsigned int, offsetT);
>>> +
>>> + aarch64_inst *base = &inst.base;
>>> + const aarch64_opcode *opcode = base->opcode;
>>> +
>>> + add_p = aarch64_opcode_subclass_p (opcode, F_ARITH_ADD);
>>> + sub_p = aarch64_opcode_subclass_p (opcode, F_ARITH_SUB);
>>> + gas_assert (add_p || sub_p);
>>> + ginsn_func = add_p ? ginsn_new_add : ginsn_new_sub;
>>> +
>>> + gas_assert (aarch64_num_of_operands (opcode) == 3);
>>> + dst = &base->operands[0];
>>> + src1 = &base->operands[1];
>>> + src2 = &base->operands[2];
>>> +
>>> + dst_reg = ginsn_dw2_regnum (dst, true);
>>> + src1_reg = ginsn_dw2_regnum (src1, true);
>>> + src2_reg = ginsn_dw2_regnum (src2, false);
>>> +
>>> + ginsn = ginsn_func (insn_end_sym, true,
>>> + GINSN_SRC_REG, src1_reg, 0,
>>> + GINSN_SRC_REG, src2_reg, 0,
>>> + GINSN_DST_REG, dst_reg, 0);
>>> + ginsn_set_where (ginsn);
>>
>> It looks like this ignores any shift that is applied to src2_reg.
>>
>
> Yes. An add/sub operation with two source registers makes REG_SP/REG_FP
> untraceable (if dest is REG_SP/REG_FP), so any further information
> carried forward in the ginsn is of little use for SCFI.
>
> For a usecase apart from SCFI, yes, this may amount to loss of vital
> information. I will add a TBD_GINSN_INFO_LOSS comment.
If SCFI doesn't handle add/sub of two registers, then do we still
need this code for correctness? I would have expected SCFI to handle
unrecognised writes to SP in a conservative way, even if there is no
ginsn type defined for the instruction that sets SP.
>>> +
>>> + ginsn = ginsn_new_mov (insn_end_sym, false,
>>> + src_type, src_reg, src_imm,
>>> + GINSN_DST_REG, dst_reg, 0);
>>> + ginsn_set_where (ginsn);
>>> +
>>> + return ginsn;
>>> +}
>>> +
>>> +/* Check if an instruction is whitelisted.
>>> +
>>> + An instruction is a candidate for whitelisting if not generating ginsn for
>>> + it, does not affect SCFI correctness.
>>> +
>>> + TBD_GINSN_GEN_NOT_SCFI. This function assumes GINSN_GEN_SCFI is in effect.
>>> + When other ginsn_gen_mode are added, this will need fixing. */
>>> +
>>> +static bool
>>> +aarch64_ginsn_safe_to_skip_p (void)
>>> +{
>>> + bool skip_p = false;
>>> + aarch64_opnd_info *opnd = NULL;
>>> + unsigned int dw2_regnum;
>>> + unsigned int opnd_reg;
>>> + int num_opnds = 0;
>>> +
>>> + aarch64_inst *base = &inst.base;
>>> + const aarch64_opcode *opcode = base->opcode;
>>> +
>>> + /* ATM, whitelisting operations with no operands does not seem to be
>>> + necessary. */
>>> + num_opnds = aarch64_num_of_operands (opcode);
>>> + if (!num_opnds)
>>> + return skip_p;
>>
>> Things like ERET show that this would be dangerous, so it might be better
>> to make the false return explicit (and adjust the comment).
>>
>
> I am not sure I understand the "make false return explicit". I have
> adjusted the code and comment:
>
> /* ATM, whitelisting operations with no operands does not seem to be
> necessary. In fact, whitelisting insns like ERET will be
> dangerous for
> SCFI. So, return false now and bar any such insns from being
> whitelisted
> altogether. */
> num_opnds = aarch64_num_of_operands (opcode);
> if (!num_opnds)
> return false;
>
> Is this what you meant ?
Yes, thanks.
>>> + if (opnd->type == AARCH64_OPND_Rd_SP)
>>> + {
>>> + dw2_regnum = ginsn_dw2_regnum (opnd, true);
>>> + if (dw2_regnum == REG_SP)
>>> + skip_p = true;
>>> + }
>>> + break;
>>> +
>>> + default:
>>> + break;
>>> + }
>>> +
>>> + return skip_p;
>>> +}
>>> +
>>> +#define AARCH64_GINSN_UNHANDLED_NONE 0
>>> +#define AARCH64_GINSN_UNHANDLED_DEST_REG 1
>>> +#define AARCH64_GINSN_UNHANDLED_CFG 2
>>> +#define AARCH64_GINSN_UNHANDLED_STACKOP 3
>>> +#define AARCH64_GINSN_UNHANDLED_UNEXPECTED 4
>>> +
>>> +/* Check the input insn for its impact on the correctness of the synthesized
>>> + CFI. Returns an error code to the caller. */
>>> +
>>> +static int
>>> +aarch64_ginsn_unhandled (void)
>>> +{
>>> + int err = AARCH64_GINSN_UNHANDLED_NONE;
>>> + aarch64_inst *base = &inst.base;
>>> + const aarch64_opcode *opcode = base->opcode;
>>> + aarch64_opnd_info *dest = &base->operands[0];
>>> + int num_opnds = aarch64_num_of_operands (opcode);
>>> + aarch64_opnd_info *addr;
>>> + unsigned int dw2_regnum;
>>> + unsigned int addr_reg;
>>> + aarch64_opnd_info *opnd = NULL;
>>> + unsigned int opnd_reg;
>>> + bool sp_allowed_p = false;
>>> +
>>> + /* All change of flow instructions are important for SCFI. */
>>> + if (opcode->iclass == condbranch
>>> + || opcode->iclass == compbranch
>>> + || opcode->iclass == testbranch
>>> + || opcode->iclass == branch_imm
>>> + || opcode->iclass == branch_reg)
>>> + err = AARCH64_GINSN_UNHANDLED_CFG;
>>> + /* Also, any memory instructions that may involve an update to the stack
>>> + pointer or save/restore of callee-saved registers must not be skipped.
>>> + Note that, some iclasses cannot be used to push or pop stack because of
>>> + disallowed writeback: ldst_unscaled, ldst_regoff, ldst_unpriv, ldstexcl,
>>> + loadlit, ldstnapair_offs. FIXME double-check.
>>> + Also, these iclasses do not seem to be amenable to being used for
>>> + save/restore ops either. FIXME double-check. */
>>> + else if (opcode->iclass == ldstpair_off
>>> + || opcode->iclass == ldstpair_indexed
>>> + || opcode->iclass == ldst_imm9
>>> + || opcode->iclass == ldst_imm10
>>> + || opcode->iclass == ldst_pos
>>> + /* STR Zn are especially complicated as they do not store in the same byte
>>> + order for big-endian: STR Qn stores as a 128-bit integer (MSB first),
>>> + whereas STR Zn stores as a stream of bytes (LSB first). FIXME Simply punt
>>> + on the big-endian and little-endian SVE PCS case for now. */
>>> + || opcode->iclass == sve_misc)
>>> + {
>>> + opnd = &base->operands[0];
>>> + addr = &base->operands[num_opnds - 1];
>>> + addr_reg = ginsn_dw2_regnum (addr, true);
>>> + opnd_reg = ginsn_dw2_regnum (opnd, false);
>>> + /* For all skipped memory operations, check if an update to REG_SP or
>>> + REG_FP is involved. */
>>> + if ((addr_reg == REG_SP || addr_reg == REG_FP)
>>> + && (((addr->addr.postind || addr->addr.preind) && addr->addr.writeback)
>>> + || aarch64_scfi_callee_saved_p (opnd_reg)))
>>> +
>>> + err = AARCH64_GINSN_UNHANDLED_STACKOP;
>>
>> The way I'd imagined this working is that we'd use two-directional data
>> flow to record which callee-saved registers are "protected" at which
>> instructions. That is, for saves we'd walk the cfg forward from the
>> entry point, recording which registers have been saved earlier in the walk
>> (and being conservatively correct for cycles). And for restores we'd
>> walk the cfg backwards from the exit points, recording which registers
>> are restored later in the walk. If a register is both "saved ealier" and
>> "restored later" for a given instruction, it wouldn't matter what the
>> instruction does with that register.
>>
>> (For infinite loops we would pretend that all registers are restored later.)
>>
>> It seems like instead, we'd generate a warning for:
>>
>> ...
>> str d8, [sp, #...]
>> ...
>> ld1d z8.b, p0/z, [sp, #...]
>> ...
>> ldr d8, [sp, #...]
>> ...
>> ret
>>
>> even though that should be ok (even without interpreting SVE instructions).
>>
>> On the other hand, any read or write of a callee-saved register is suspicious
>> if it isn't saved earlier or isn't restored later.
>>
>> In other words, it seems like this information should be context-dependent,
>> or used in a context-dependent way. Whether a particular register matters
>> depends on whether the register might still contain the caller's data.
>>
>
> We do something similar for non-memory ops:
> - In the aarch64_ginsn_unhandled () codepath, if the destination is of
> interest (REG_SP, REG_FP), synthesize a GINSN_TYPE_OTHER with the
> appropriate destination register if no ginsn has been generated.
> - The SCFI machinery checks if this instruction affects SCFI correctness.
> - If it does, only then the insn is issued a complaint for and brought
> to attention.
>
> What you suggest is take a similar approach for memory operations too.
> I think it makes sense. It needs some work now to accommodate this, but
> I think we can re-evaluate after some basic handling for Z registers is
> in place ? I plan to work on adding handling for Z registers after this
> series is merged. (I have renamed an old testcase involving Z registers
> to "scfi-unsupported-2.s" to keep track of the currently unsupported
> cases to be worked on in near future, so I dont lose track of this..).
Yeah, leaving it till later sounds ok. But I don't think this should
depend on recognising Z loads and stores. It should just depend on
processing Z registers in the operand lists, and recognising that the
low 64 bits of Z8-Z15 are the same as D8-D15.
In other words, I imagined the set-up would be:
(1) the operand list should tell us whether a register is referenced
(2) recognising 8-byte+ loads and stores, plus support arithmetic,
lets us recognise instructions that would function as a save or
restore (and at what offset)
(3) dataflow analysis based on (2) tells us, for a given instruction,
which registers might have caller data
(4) A reference to possible caller data triggers a warning
(references detected by (1), caller data by (3))
Is that roughly how it works?
Thanks,
Richard
next prev parent reply other threads:[~2024-07-11 13:15 UTC|newest]
Thread overview: 29+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-07-01 2:53 [PATCH,V4 0/8] Add SCFI support for aarch64 Indu Bhagat
2024-07-01 2:53 ` [PATCH,V4 1/8] gas: scfi: make scfi_state_restore_reg function more precise Indu Bhagat
2024-07-12 15:03 ` [PATCH, V4 " Indu Bhagat
2024-07-01 2:53 ` [PATCH,V4 2/8] include: opcodes: aarch64: define new subclasses Indu Bhagat
2024-07-01 17:40 ` Richard Sandiford
2024-07-11 5:14 ` Indu Bhagat
2024-07-11 12:22 ` Richard Sandiford
2024-07-11 17:59 ` Indu Bhagat
2024-07-01 2:53 ` [PATCH,V4 3/8] opcodes: aarch64: flags to denote subclasses of ldst insns Indu Bhagat
2024-07-01 18:06 ` Richard Sandiford
2024-07-11 5:45 ` Indu Bhagat
2024-07-12 13:59 ` Indu Bhagat
2024-07-13 7:34 ` Indu Bhagat
2024-07-01 2:54 ` [PATCH,V4 4/8] opcodes: aarch64: flags to denote subclasses of arithmetic insns Indu Bhagat
2024-07-01 18:13 ` Richard Sandiford
2024-07-11 5:47 ` Indu Bhagat
2024-07-11 12:52 ` Richard Sandiford
2024-07-11 17:58 ` Indu Bhagat
2024-07-11 18:43 ` Richard Sandiford
2024-07-12 12:53 ` Indu Bhagat
2024-07-01 2:54 ` [PATCH,V4 5/8] opcodes: aarch64: flags to denote subclasses of uncond branches Indu Bhagat
2024-07-01 2:54 ` [PATCH,V4 6/8] opcodes: aarch64: enforce checks on subclass flags in aarch64-gen.c Indu Bhagat
2024-07-01 2:54 ` [PATCH,V4 7/8] gas: aarch64: add experimental support for SCFI Indu Bhagat
2024-07-01 19:49 ` Richard Sandiford
2024-07-11 6:30 ` Indu Bhagat
2024-07-11 13:15 ` Richard Sandiford [this message]
2024-07-11 19:07 ` Indu Bhagat
2024-07-11 20:10 ` Richard Sandiford
2024-07-01 2:54 ` [PATCH,V4 8/8] gas: aarch64: testsuite: add new tests " Indu Bhagat
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=mptr0c0ul1t.fsf@arm.com \
--to=richard.sandiford@arm.com \
--cc=Richard.Earnshaw@arm.com \
--cc=binutils@sourceware.org \
--cc=indu.bhagat@oracle.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).