[PATCH,V4 10/14] gas: synthesize CFI for hand-written asm
Indu Bhagat
indu.bhagat@oracle.com
Wed Jan 10 11:26:41 GMT 2024
On 1/10/24 01:44, Jan Beulich wrote:
> On 10.01.2024 07:10, Indu Bhagat wrote:
>> On 1/9/24 01:30, Jan Beulich wrote:
>>> On 08.01.2024 20:33, Indu Bhagat wrote:
>>>> On 1/5/24 05:58, Jan Beulich wrote:
>>>>> On 03.01.2024 08:15, Indu Bhagat wrote:
>>>>>> +/* Check whether a '66H' prefix accompanies the instruction.
>>>>>
>>>>> With APX 16-bit operand size isn't necessarily represented by a 66h
>>>>> prefix, but perhaps with an "embedded prefix" inside the EVEX one.
>>>>> Therefore both the comment and even more so ...
>>>>>
>>>>>> + The current users of this API are in the handlers for PUSH, POP
>>>>>> + instructions. These instructions affect the stack pointer implicitly: the
>>>>>> + operand size (16, 32, or 64 bits) determines the amount by which the stack
>>>>>> + pointer is decremented (2, 4 or 8). When '66H' prefix is present, the
>>>>>> + instruction has a 16-bit operand. */
>>>>>> +
>>>>>> +static bool
>>>>>> +ginsn_prefix_66H_p (i386_insn insn)
>>>>>
>>>>> ... the function name would better not allude to just the legacy
>>>>> encoding. Maybe ginsn_opsize_prefix_p()?
>>>>>
>>>>
>>>> Isnt 66H_p more readable and easier to follow because that's what the
>>>> function is currently checking ? If more scenarios were being handled,
>>>> ginsn_opsize_prefix_p () would fit better.
>>>
>>> Well, as said - with APX you can't get away with just 0x66 prefix checking.
>>> That prefix is simply illegal to use with EVEX-encoded insns.
>>>
>>
>> I am using the following in ginsn_opsize_prefix_p ():
>>
>> !(i.prefix[REX_PREFIX] & REX_W) && i.prefix[DATA_PREFIX] == 0x66
>
> That addresses one half of my earlier remarks. Note however that elsewhere
> we never check i.prefix[DATA_PREFIX] against being 0x66; we only ever check
> for it being zero or non-zero. I'd like this to remain consistent.
>
> For EVEX-encoded APX insns this isn't going to be sufficient though. See
> respective code in process_suffix():
>
> /* The DATA PREFIX of EVEX promoted from legacy APX instructions
> needs to be adjusted. */
> if (i.tm.opcode_space == SPACE_EVEXMAP4)
> {
> gas_assert (!i.tm.opcode_modifier.opcodeprefix);
> i.tm.opcode_modifier.opcodeprefix = PREFIX_0X66;
> }
> else if (!add_prefix (prefix))
> return 0;
>
> So you'll need to also check for that combination, plus take care of
> avoiding insns where PREFIX_0X66 alters operation, not operand size
> (ADCX being an example).
>
But I am now disallowing APX insns for now, altogether, by doing this in
the x86_ginsn_new:
/* Until it is clear how to handle APX NDD and other new opcodes, disallow
them from SCFI. */
if (is_apx_rex2_encoding ()
|| (i.tm.opcode_modifier.evex && is_apx_evex_encoding ()))
{
as_bad (_("SCFI: unsupported APX op %#x may cause incorrect CFI"),
opcode);
return ginsn;
}
>>>>>> +static ginsnS *
>>>>>> +x86_ginsn_move (const symbolS *insn_end_sym)
>>>>>> +{
>>>>>> + ginsnS *ginsn;
>>>>>> + unsigned int dst_reg;
>>>>>> + unsigned int src_reg;
>>>>>> + offsetT src_disp = 0;
>>>>>> + offsetT dst_disp = 0;
>>>>>> + const reg_entry *dst = NULL;
>>>>>> + const reg_entry *src = NULL;
>>>>>> + uint16_t opcode = i.tm.base_opcode;
>>>>>> + enum ginsn_src_type src_type = GINSN_SRC_REG;
>>>>>> + enum ginsn_dst_type dst_type = GINSN_DST_REG;
>>>>>> +
>>>>>> + if (opcode == 0x8b || opcode == 0x8a)
>>>>>
>>>>> Above when handling ALU insns you didn't care about byte ops - why do
>>>>> you do so here (by checking for 0x8a, and 0x88 below)?
>>>>
>>>> Because there may be weird code that saves and restores 8-byte registers
>>>> on stack. For ALU insns, if the destination is REG_SP/REG_FP, we will
>>>> detect them in the unhandled track.
>>>
>>> You talk about 8-byte registers when I asked about byte reg moves. If
>>> there's a MOV targeting %spl or %bpl, is that really any different from,
>>> say, an ADD doing so?
>>>
>>
>> ATM, Yes. SCFI has heuristics implemented, _some_ of which (can be
>> evolved) are opcode specific. E.g.,
>> - MOV %rsp, %rbp will make SCFI machinery check if this is making the
>> CFA switch to %rbp. If the target was %bpl, since we track 8-byte
>> registers, it will still trigger the same code path. A bug as I ack below.
>> - ADD rsp + 0 = rbp will not trigger CFA switch to RBP. Should it ?
>> Perhaps yes? Its on my TODO list (with low priority) to evolve this bit.
>
> I'm not sure about this. Either way such special cases may want documenting
> explicitly.
>
OK. There is also need to document ginsn, SCFI workings with examples
of unsupported cases etc. I will invest some time on this.
>>>>>> + }
>>>>>> +
>>>>>> + ginsn_set_where (ginsn);
>>>>>> +
>>>>>> + return ginsn;
>>>>>> +}
>>>>>
>>>>> Throughout this function (and perhaps others as well), how come you
>>>>> don't consider operand size at all? It matters whether results are
>>>>> 64-bit, 32-bit, or 16-bit, after all.
>>>>
>>>> Operation size matters: No, not for all instructions in context of SCFI.
>>>> For instructions using stack (save / restore), the size is important.
>>>> But for other instructions, operation size will not affect SCFI correctness.
>>>
>>> But aren't you wrongly treating an update of %rbp and an update of,
>>> say, %bp the same then? The latter should end up as untracable, aiui.
>>>
>>
>> For ALU ops:
>> - ADD/SUB reg1, reg2
>> If reg2 is the same as REG_CFA, then even in 64-bit mode, this
>> causes untraceability. So there is untraceability irrespective of
>> operation size. On the other hand, if uninteresting, it will remain
>> unintersting irrespective of operation size.
>> - ADD/SUB imm, reg will never trigger untraceability, irrespective of
>> size. There is the element of ignoring the carry bit, but I think the
>> current diagnostics around "asymmetrical save/restore" and the planned
>> "unbalanced stack at return" should provide user with some safety net.
>
> I don't see how the carry flag would matter here. What does matter is the
> size of the register: Anything not 64-bit will need to trigger
> untraceability imo.
>
- For a 16-bit ADD/SUB imm, reg insn: if the 16-bit result had a
carry, it will be ignored as only 16-bit result is copied to the
destination address IIUC. If there is no carry bit, 16-bit ADD/SUB imm,
reg is semantically the same insn as a 64-bit ADD/SUB imm, reg for the
same immediate.
- For 32-bit ADD/SUB imm, reg insn: yes, I stand corrected. It should
trigger untraceability as upper 32-bits are zeroed out. IMO, this 32-bit
operation should likely be unintentional by the user; something better
alerted to the user if we can.
I will come back to this item of tracking operation sizes when I fix the
MOV issue below.
>> - Other ALU ops should all trigger untracebility alike, irrespective
>> of operation size.
>> Hence, my statement that ignoring operation size is fine here for SCFI.
>
> As per above, I disagree.
>
>> For MOV ops:
>> - 8-bit/16-bit MOV should trigger untracebility. I ack this as bug to
>> be fixed. Thanks to your careful review. ATM, I plan to deal with this
>> after the series goes in, unless you have strong opinion about this.
>
> 32-bit MOV should, too. And yes, I'm okay-ish with such being dealt with
> later, as long as the open work item is easily recognizable (and hence
> it'll be easy to check that all such work items are gone at the point
> where the feature is promoted from experimental).
>
Right, 32-bit MOV should too.
>>>>>> +/* Generate one or more generic GAS instructions, a.k.a, ginsns for the current
>>>>>> + machine instruction.
>>>>>> +
>>>>>> + Returns the head of linked list of ginsn(s) added, if success; Returns NULL
>>>>>> + if failure.
>>>>>> +
>>>>>> + The input ginsn_gen_mode GMODE determines the set of minimal necessary
>>>>>> + ginsns necessary for correctness of any passes applicable for that mode.
>>>>>> + For supporting the GINSN_GEN_SCFI generation mode, following is the list of
>>>>>> + machine instructions that must be translated into the corresponding ginsns
>>>>>> + to ensure correctness of SCFI:
>>>>>> + - All instructions affecting the two registers that could potentially
>>>>>> + be used as the base register for CFA tracking. For SCFI, the base
>>>>>> + register for CFA tracking is limited to REG_SP and REG_FP only for
>>>>>> + now.
>>>>>> + - All change of flow instructions: conditional and unconditional branches,
>>>>>> + call and return from functions.
>>>>>> + - All instructions that can potentially be a register save / restore
>>>>>> + operation.
>>>>>
>>>>> This could do with being more fine grained, as "potentially" is pretty vague,
>>>>> and (as per earlier version review comments) my take on this is a much wider
>>>>> set than yours.
>>>>
>>>> I would like to understand more on this comment, especially the "my take
>>>> on this is a much wider set than yours". I see its being hinted at in
>>>> different flavors in the current review.
>>>>
>>>> I see some issues pointed out in this review (addressing modes of mov
>>>> etc, safe to skip opcodes for TEST, CMP) etc., but it seems that your
>>>> concerns are wider than this.
>>>
>>> I earlier version review I mentioned that even vector or mask registers
>>> could in principle be use to hold preserved GPR values. I seem to recall
>>> that you said you wouldn't want to deal with such. Hence my use of
>>> "wider set": Just to give an example, "kmovq %rbp, %k0" plus later
>>> "kmovq %k0, %rbp" is a pair of "instructions that can potentially be a
>>> register save / restore operation".
>>>
>>
>> Hmm. I will need to understand them on a case to case basis. For the
>> case of "kmovq %rbp, %k0" / "kmovq %k0, %rbp" how can this be used as
>> save/restore to/from stack ?
>
> Maybe I'm still not having a clear enough picture of what forms of insns
> you want to fully track. Said insn forms don't access the stack. But they
> could in principle be used to preserve a certain register. Such preserving
> of registers is part of what needs encoding in CFI, isn't it?
>
The kind of preserving is usually on stack. It can also be in another
callee-saved register, in theory, but the latter defeats the purpose of
state saving across calls.
BTW, I had earlier written down some notes about SCFI
https://sourceware.org/pipermail/binutils/2023-September/129558.html
Some bits are stale already though, but may be it helps.
>>>>>> + ginsn = ginsn_new_load (insn_end_sym, false,
>>>>>> + GINSN_SRC_INDIRECT, REG_SP, 0,
>>>>>> + GINSN_DST_REG, dw2_regnum);
>>>>>> + ginsn_set_where (ginsn);
>>>>>> + ginsn_next = ginsn_new_add (insn_end_sym, false,
>>>>>> + GINSN_SRC_REG, REG_SP, 0,
>>>>>> + GINSN_SRC_IMM, 0, stack_opnd_size,
>>>>>> + GINSN_DST_REG, REG_SP, 0);
>>>>>> + ginsn_set_where (ginsn_next);
>>>>>> + gas_assert (!ginsn_link_next (ginsn, ginsn_next));
>>>>>> + break;
>>>>>> +
>>>>>> + case 0xff:
>>>>>> + if (i.tm.opcode_space != SPACE_BASE)
>>>>>> + break;
>>>>>> + /* push from mem. */
>>>>>> + if (i.tm.extension_opcode == 6)
>>>>>> + {
>>>>>> + /* In 64-bit mode, presence of 66H prefix indicates 16-bit op. */
>>>>>> + if (flag_code == CODE_64BIT && ginsn_prefix_66H_p (i))
>>>>>> + stack_opnd_size = 2;
>>>>>> + ginsn = ginsn_new_sub (insn_end_sym, false,
>>>>>> + GINSN_SRC_REG, REG_SP, 0,
>>>>>> + GINSN_SRC_IMM, 0, stack_opnd_size,
>>>>>> + GINSN_DST_REG, REG_SP, 0);
>>>>>> + ginsn_set_where (ginsn);
>>>>>> + /* These instructions have no imm, only indirect access. */
>>>>>> + gas_assert (i.base_reg);
>>>>>> + dw2_regnum = ginsn_dw2_regnum (i.base_reg);
>>>>>> + ginsn_next = ginsn_new_store (insn_end_sym, false,
>>>>>> + GINSN_SRC_INDIRECT, dw2_regnum,
>>>>>> + GINSN_DST_INDIRECT, REG_SP, 0);
>>>>>> + ginsn_set_where (ginsn_next);
>>>>>> + gas_assert (!ginsn_link_next (ginsn, ginsn_next));
>>>>>> + }
>>>>>> + else if (i.tm.extension_opcode == 4)
>>>>>> + {
>>>>>> + /* jmp r/m. E.g., notrack jmp *%rax. */
>>>>>> + if (i.reg_operands)
>>>>>> + {
>>>>>> + dw2_regnum = ginsn_dw2_regnum (i.op[0].regs);
>>>>>> + ginsn = ginsn_new_jump (insn_end_sym, true,
>>>>>> + GINSN_SRC_REG, dw2_regnum, NULL);
>>>>>> + ginsn_set_where (ginsn);
>>>>>> + }
>>>>>> + else if (i.mem_operands && i.index_reg)
>>>>>> + {
>>>>>> + /* jmp *0x0(,%rax,8). */
>>>>>> + dw2_regnum = ginsn_dw2_regnum (i.index_reg);
>>>>>> + ginsn = ginsn_new_jump (insn_end_sym, true,
>>>>>> + GINSN_SRC_REG, dw2_regnum, NULL);
>>>>>> + ginsn_set_where (ginsn);
>>>>>
>>>>> What if both base and index are in use? Like for PUSH/POP, all addressing
>>>>> forms are permitted here and ...
>>>>>
>>>>>> + }
>>>>>> + else if (i.mem_operands && i.base_reg)
>>>>>> + {
>>>>>> + dw2_regnum = ginsn_dw2_regnum (i.base_reg);
>>>>>> + ginsn = ginsn_new_jump (insn_end_sym, true,
>>>>>> + GINSN_SRC_REG, dw2_regnum, NULL);
>>>>>> + ginsn_set_where (ginsn);
>>>>>> + }
>>>>>> + }
>>>>>> + else if (i.tm.extension_opcode == 2)
>>>>>> + {
>>>>>> + /* 0xFF /2 (call). */
>>>>>> + if (i.reg_operands)
>>>>>> + {
>>>>>> + dw2_regnum = ginsn_dw2_regnum (i.op[0].regs);
>>>>>> + ginsn = ginsn_new_call (insn_end_sym, true,
>>>>>> + GINSN_SRC_REG, dw2_regnum, NULL);
>>>>>> + ginsn_set_where (ginsn);
>>>>>> + }
>>>>>> + else if (i.mem_operands && i.base_reg)
>>>>>> + {
>>>>>> + dw2_regnum = ginsn_dw2_regnum (i.base_reg);
>>>>>> + ginsn = ginsn_new_call (insn_end_sym, true,
>>>>>> + GINSN_SRC_REG, dw2_regnum, NULL);
>>>>>> + ginsn_set_where (ginsn);
>>>>>> + }
>>>>>
>>>>> ... here.
>>>>
>>>> For the indirect jump and call instructions, the target destination is
>>>> not necessary to be known. Indirect jumps will cause SCFI to error out
>>>> as control flow cannot be constructed.
>>>>
>>>> Call instructions are assumed to transfer control out of function.
>>>>
>>>> In other words, more information in these cases will not be of use to SCFI.
>>>
>>> But then aren't you already doing too much work here? With what you say,
>>> you don't care about the kind of operand at all, merely the fact it's a
>>> CALL might be interesting. Albeit even there I'd then wonder why - if
>>> the function called isn't of interest, how is CALL different from just
>>> NOP? The only CALL you'd need to be concerned about would be the direct
>>> form with a displacement of 0, as indicated elsewhere. But of course
>>> tricky code might also use variants of that; see e.g. the retpoline code
>>> that was introduced into kernels a couple of years back. Recognizing
>>> and bailing on such tricky code may be desirable, if not necessary.
>>>
>>
>> Tracking operands for CALL instructions does not provide value ATM. We
>> do not even split a BB if there is a CALL instruction (the assumption is
>> that CALL insn transfers control out of function).
>>
>> You're right that we need to treat some CALLs differently (because of
>> its affect of control flow and SCFI correctness).
>>
>> RE: doing more work. Sure, but the vision for ginsn was to allow a
>> representation where other analyses may be added. The representation of
>> ginsn will need revisions to get there, but keeping an explicit
>> GINSN_TYPE_CALL seems good.
>
> That's okay, but with as little dead (for now) code as possible. If the
> operand isn't interesting, why deal with the various operand forms right
> now?
>
I dont have a good answer to this. If I dont add opnds to
GINSN_TYPE_CALL now, I will have to put some dummy default args (which I
personally find confusing to see; I already have some in the code
elsewhere for RegIP/RegIZ). And yes, we have disagreed on this matter
before too.
>>>>>> + }
>>>>>> + break;
>>>>>> +
>>>>>> + case 0xc2:
>>>>>> + case 0xc3:
>>>>>> + if (i.tm.opcode_space != SPACE_BASE)
>>>>>> + break;
>>>>>> + /* Near ret. */
>>>>>> + ginsn = ginsn_new_return (insn_end_sym, true);
>>>>>> + ginsn_set_where (ginsn);
>>>>>> + break;
>>>>>
>>>>> No tracking of the stack pointer adjustment?
>>>>
>>>> No stack unwind information for a function is relevant after the
>>>> function has returned. So, tracking of stack pointer adjustment by
>>>> return is not necessary.
>>>
>>> What information does the "return" insn then carry, beyond it being
>>> an unconditional branch (which you have a different insn for)?
>>>
>>
>> "return" does not carry any more information than just the
>> GINSN_TYPE_RETURN as ginsn->type.
>>
>> So then why support both "return" and an unconditional branch: The
>> intention is to carry the semantic difference between ret and
>> unconditional jump. Unconditional jumps may be to a label within
>> function, and in those cases, we use it for some validation and BB
>> linking when creating CFG. Return, OTOH, always indicates exit from
>> function.
>>
>> For SCFI purposes, above is the one use. Future analyses may find other
>> use-cases for an explicit return ginsn. But IMO, keeping
>> GINSN_TYPE_RETURN as an explicit insn makes the overall offering cleaner.
>
> Okay. And here you don't bother decoding operands. Hence why I'm
> asking the same to be the case for (e.g.) CALL.
>
It seems I will need to deal with operands of RETURN insn soon. For
implementing "Warn if imbalanced stack at return", we will need this info.
>>>>>> @@ -5830,6 +6804,14 @@ md_assemble (char *line)
>>>>>> /* We are ready to output the insn. */
>>>>>> output_insn (last_insn);
>>>>>>
>>>>>> + /* PS: SCFI is enabled only for AMD64 ABI. The ABI check has been
>>>>>> + performed in i386_target_format. */
>>>>>
>>>>> See my earlier comment - it's yet more restrictive (as in not covering
>>>>> e.g. the Windows ABI, which importantly is also used in EFI).
>>>>
>>>> Not clear, can you please elaborate ?
>>>
>>> Hmm, it's not clear to me what's not clear in my earlier comment. The
>>> set of preserved registers, for example, differs between the SysV and
>>> the Windows ABIs (see x86_scfi_callee_saved_p()). Then again, thinking
>>> of it, talking of anything ABI-ish in assembly code is questionable.
>>> An assembly function calling another assembly function may use an
>>> entirely custom "ABI". You just cannot guess upon preserved registers.
>>>
>>
>> I think the confusion is stemming from my usage of AMD64 colloquially to
>> refer to System V ABI for x86_64. I think "System V AMD64 ABI" is the
>> correct term. I will use that.
>>
>> And yes, GAS can only work out SCFI if there is ABI adherence. If input
>> asm does not adhere to the supported ABIs, and the user invokes SCFI,
>> then the user is on their own. GAS will not (rather can not) warn about
>> ABI incompliance.
>
> I don't recall documentation stating this explicitly. This is a pretty
> important aspect for users to consider, after all.
>
I added a reference in gas/NEWS for now.
* Experimental support in GAS to synthesize CFI for ABI-conformant,
hand-written asm using the new command line option
--scfi=experimental on x86-64.
I will add a mention of "ABI conformance" in as.texi.
More information about the Binutils
mailing list