[llvm-dev] [RFC] Introducing a byte type to LLVM

Nuno Lopes via llvm-dev llvm-dev at lists.llvm.org
Fri Jun 4 12:17:19 PDT 2021


It's true: an alternative to introducing a new byte type is to make alias analysis more conservative. But I think it's way worse than you think.
Making AA conservative means that every single integer load must be considered to be escaping a pointer. Unless you know the last stored value was an integer, you would have to conservatively assume that the load may be escaping some pointer (even if only partially). This sounds to me like terrible. Type punning through memory is probably very uncommon, and it seems we would be slowing down all programs because of such uncommon case.

Therefore I feel it's best if we introduce a way to explicitly mark potential type punning situations, such that AA (and optimizers) only need to be conservative when needed. It's not the case that right now we can detect the few potential type punning cases and be conservative just there. We would need to be conservative in e.g. every function that loads an argument pointer.
I agree it's scary to introduce a new type in the IR; we don't do that very often. But we need to fix the bugs in LLVM.

Nuno
 

-----Original Message-----
From: Johannes Doerfert via llvm-dev
Sent: 04 June 2021 17:56 
Subject: Re: [llvm-dev] [cfe-dev] [RFC] Introducing a byte type to LLVM


On 6/4/21 11:35 AM, George Mitenkov wrote:
> Hi Johannes,
>
> Sure! The underlying problem is that raw-memory access handlers are 
> treated as integers, while they are not really integers. Especially 
> std::byte that specifically states that it has raw-memory access 
> semantics. This semantic mismatch can make AA wrong and a pointer to 
> escape.

I would assume being conservative is a possibility here, right?
So if someone wants to type-pun a pointer we would do very little optimizations around it.


>
> Consider the following LLVM IR that copies a pointer:
>
> %src8 = bitcast i8** %src to i8*
>
> %dst8 = bitcast i8** %dst to i8*
>
> call void @llvm.memcpy.p0i8.p0i8.i32(i8* %dst8, i8* %src8, i32 8, i1 
> false)
>
> %load = load i8*, i8** %dst
>
> %addr = ptrtoint i8* %load to i64
>
> ret i64 %addr
>
>
> If we optimize the call to memcpy, then the IR becomes
>
>
> %src64 = bitcast i8** %src to i64*
>
> %dst64 = bitcast i8** %dst to i64*
>
> %addr = load i64, i64* %src64, align 1
>
> store i64 %addr, i64* %dst64, align 1
>
> ret i64 %addr
>
>
> Since there is no "data" type in LLVM like a byte, the ptrtoint is 
> optimized out and the pointer escapes. If we have used a pair of byte 
> load/store that would not happen.

The pointer (load %src) does escape in this example, doesn't it?

I don't get why "that would not happen" with byte load/store and why that would be correct at all.


> The mentioned bug is just an example, but this pattern can be
>
> replicated in other cases that optimize memcpy as integer load/stores and
> drop ptrtoint,
>
> undermining alias analysis. To illustrate further, consider loading a
> pointer like:
>
>
> %v1 = load i8*, %p
>
> %v2 = load i8*, %p
>
>
> Instcombine optimizes this into
>
>
> %v1 = load i64, %p
>
> %v2 = inttoptr %v1
>
>
> changing the underlying provenance (again, not good for AA).

This example looks somehow broken (I'll assuming %v1 has integer type).

I agree this is not a great optimization, but is it wrong? We should have
done the opposite (ptr2int for v1) but even then I'm not sure what AA could
derive here. We load a pointer and then we escape it into an integer. If the
ptr2int is not removed, I'm not sure what AA should do about this. I'm also
not sure how practical this is. Do we have some idea how often such 
cases exist?


>   If we had a
> byte type instead,
>
> then we could
>
> 1. preserve provenance
>
> 2. differentiate between integers and the raw data copied
>
>
> This might be beneficial for some backends (e.g CHERI) that want to
> differentiate between
>
> true integers and bytes that can be integers. The optimizations for known
> integers
>
> can become more aggressive in this case.
>
It sounds to me like the problem is our optimization in the presence of
int2ptr and ptr2int are (still) unsound. We should at least not remove 
ptr2int,
is that it?

That said, I still don't get how a byte type helps for these examples. 
More concretely:

ref 1) How does a byte type preserve provenance? If it is somehow 
implicitly attached
        would that not mean we need to now be careful doing 
optimizations on bytes (as we
        should now be careful doing optimizations around p2i and i2p)? 
Literally shifting
        the problem from looking for p2i and i2p to looking for bytecast?

ref 2) What optimization can I do if I have the differentiation of 
integers and raw data?
        Can we somehow get an idea of the optimization potential?


~ Johannes



> Thanks,
>
> George
>
> On Fri, Jun 4, 2021 at 7:03 PM Johannes Doerfert <johannesdoerfert at gmail.com>
> wrote:
>
>> Hi George,
>>
>> On 6/4/21 10:24 AM, George Mitenkov via cfe-dev wrote:
>>> Hi all,
>>>
>>> Together with Nuno Lopes and Juneyoung Lee we propose to add a new byte
>>> type to LLVM to fix miscompilations due to load type punning. Please see
>>> the proposal below. It would be great to hear the
>>> feedback/comments/suggestions!
>>>
>>>
>>> Motivation
>>> ==========
>>>
>>> char and unsigned char are considered to be universal holders in C. They
>>> can access raw memory and are used to implement memcpy. i8 is the LLVM’s
>>> counterpart but it does not have such semantics, which is also not
>>> desirable as it would disable many optimizations.
>>>
>>> We therefore propose to introduce a new byte type that would have a
>> raw-memory
>>> access semantics and can be differentiated from i8. This can help to fix
>>> unsound optimizations and make the lowering of char, unsigned char or
>>> std::byte correct. Currently, we denote the byte type as b<N>, where N is
>>> the number of bits.
>>>
>>> In our semantics, byte type carries provenance and copying bytes does not
>>> escape pointers, thereby benefiting alias analysis. If the memory
>> contains
>>> poison bits, then the result of loading a byte from it is bitwise poison.
>> Could you elaborate on the motivation a bit. I'm not sure I follow.
>> Maybe examples would be good. The only one I found is in
>> https://bugs.llvm.org/show_bug.cgi?id=37469. For that one I'm
>> wondering how much we would in reality loose if we make the required
>> ptr2int explicit when people cast a pointer to an integer. In general,
>> explicit encoding seems to me preferable and overall less work (new
>> types kinda scare me TBH).
>>
>> ~ Johannes
>>
>>
>>>
>>> Benefits
>>> ========
>>>
>>> The small calls to memcpy and memmove that are transformed by Instcombine
>>> into integer load/store pairs would be correctly transformed into the
>>> loads/stores of the new byte type no matter what underlying value is
>> being
>>> copied (integral value or a pointer value).
>>>
>>>      1.
>>>
>>>      The new byte type solves the problem of copying the padded data,
>> with no
>>>      contamination of the loaded value due to poison bits of the padding.
>>>      2.
>>>
>>>      When copying pointers as bytes implicit pointer-to-integer casts are
>>>      avoided.The current setting of performing a memcpy using i8s leads to
>>>      miscompilations (example: bug report 37469
>>>      <https://bugs.llvm.org/show_bug.cgi?id=37469>) and is bad for alias
>>>      analysis.
>>>
>>>
>>>
>>> Lowering of char, unsigned char and std::byte
>>>
>>> ======================================
>>>
>>> For any function that takes char, unsigned char or std::byte as
>> arguments,
>>> we lower these to the b8 instead of i8. The motivation comes from the
>> fact
>>> that any pointer can be explicitly copied in bytes, and each byte can be
>>> passed as an argument to the copying function. Since a copy like that can
>>> escape a pointer, we need to generate the byte type to avoid the issue.
>>>
>>> Example
>>>
>>> void foo(unsigned char arg1, char arg2) {...}
>>>
>>>               =>
>>>
>>> void @foo(zeroext b8 %arg1, signext b8 %arg2) {...}
>>>
>>>
>>>
>>> std::byte is defined as enum class byte : unsigned char {} and therefore
>>> all bitwise operations over it can be lowered equivalently to operations
>> on
>>> unsigned char (operation is performed over zero-extended to i32 operands,
>>> and the result is truncated back).
>>>
>>>
>>> ==============================
>>>
>>> Byte Type Semantics (Version 1)
>>>
>>> ==============================
>>>
>>>
>>> [allowed] alloca/load/store
>>>
>>> =====================
>>>
>>> The byte type is allowed to be allocated, stored and loaded. No semantic
>>> changes needed.
>>>
>>> [allowed] zext/sext/trunc
>>>
>>> ====================
>>>
>>> In order to easily adapt the current replacement of i8 with b8, we want
>> to
>>> extend zext/sext/trunc instructions’ semantics to allow the first operand
>>> as a byte type as well. This will reduce the number of instructions
>> needed
>>> for the cast.
>>>
>>> Example
>>>
>>> trunc i32 %int to b8
>>>
>>> zext i8 %char to b32
>>>
>>> [modified] bitcast
>>>
>>> ==============
>>>
>>> We modify the semantics of the bitcast to allow casts from and to the
>> byte
>>> types. If the byte has a value of the same kind (integral or a pointer)
>> as
>>> the other type, then the bitcast is a noop. Otherwise, the result is
>> poison.
>>> Example
>>>
>>> bitcast i<N> %val to b<N>
>>>
>>> bitcast i<N>* %val to b64
>>>
>>> bitcast b<N> %byte to i<N>     [byte has an integral value => noop]
>>>
>>>
>>>
>>> [new] bytecast
>>>
>>> ============
>>>
>>> During IR generation, we cannot deduce whether the byte value contains a
>>> pointer or an integral value, and therefore we cannot create a bitcast.
>>>
>>> Example
>>>
>>> int @cast(char c) { return (int) c; }
>>>
>>> Current
>>>
>>> Proposed
>>>
>>> i32 @cast(i8 signext %c) {
>>>
>>>     %1 = alloca i8, align 1
>>>
>>>     store i8 %c, i8* %1, align 1
>>>
>>>     %2 = load i8, i8* %1, align 1
>>>
>>>     %3 = sext i8 %2 to i32
>>>
>>>     ret i32 %3
>>>
>>> }
>>>
>>> i32 @cast(b8 %c) {
>>>
>>>     %1 = alloca b8
>>>
>>>     store b8 %c, b8* %11
>>>
>>>     %2 = load b8, b8* %1
>>>
>>>     %3 =    ?    b8 %2 to i32
>>>
>>>     ret i32 %3
>>>
>>> }
>>>
>>> In this example, the operation  ?  can be sext (b8 is i8) or can be
>>> ptrtoint if the byte has a pointer value.
>>>
>>> We therefore introduce a new bytecast instruction. The frontend will
>> always
>>> produce a bytecast, which can then be optimized into a more specific cast
>>> if necessary. Bytecast operands must have the same bitwidth (or size).
>>>
>>> The semantics of bytecast instruction is:
>>>
>>> bytecast b<N> %byte to T
>>>
>>> The kind of the value of the byte
>>>
>>> T
>>>
>>> Semantics
>>>
>>> Pointer
>>>
>>> Integral
>>>
>>> ptrtoint
>>>
>>> Pointer
>>>
>>> Pointer
>>>
>>> bitcast
>>>
>>> Integral
>>>
>>> Integral
>>>
>>> integral casts (zext, sext, etc.)
>>>
>>> Integral
>>>
>>> Pointer
>>>
>>> inttoptr
>>>
>>>
>>>
>>> Essentially, this instruction is a selection of the right casts. Once the
>>> compiler has been able to prove the kind of the byte’s value, the
>> bytecast
>>> can be replaced with appropriate cast or become a noop.
>>>
>>> Example
>>>
>>> bytecast b64 %bytes to i32*
>>>
>>> bytecast b8 %byte to i8
>>>
>>>
>>>
>>> [disabled] and/or/xor
>>>
>>> =================
>>>
>>> We do not allow bitwise operations over byte types because it’s not
>> trivial
>>> to define all cases, particularly the ones involving pointers. If these
>>> operations are useful for optimizations and/or some frontends, semantics
>>> for these can be considered separately.
>>>
>>> If the operation is desired on the frontend level, then the default
>>> generated code can always cast the byte type to an integer to operate on
>>> integer values.
>>>
>>>
>>>
>>> [disabled] arithmetic
>>>
>>> =================
>>>
>>> Performing arithmetic operations over the byte type is similarly not
>>> allowed (A valid question is what does it mean to add to bytes of raw
>>> memory?). If we want to perform arithmetic, we need to cast a byte to an
>>> integer (via sext/zext/trunc explicitly).
>>>
>>>
>>>
>>> [modified] comparison
>>>
>>> ==================
>>>
>>> We allow performing comparisons, as we may potentially want to compare
>> the
>>> ordering of the memory instances, check for null, etc. Comparison is also
>>> needed since char types are commonly compared. We define the following
>>> semantic of the byte type comparison.
>>>
>>> Case 1: The values of the bytes have the same kinds: compare the values.
>>>
>>> Case 2: The values of the bytes have the different kinds: do a bytecast
>> of
>>> the non-integral type to the other type and compare the integral values.
>>>
>>>
>>> Example
>>>
>>> =======
>>>
>>> unsigned char sum(unsigned char a, unsigned char b) { return a + b; }
>>>
>>> Current
>>>
>>> Proposed
>>>
>>> zeroext i8 @sum(i8 zeroext %a, i8 zeroext %b) {
>>>
>>>     %1 = alloca i8
>>>
>>>     %2 = alloca i8
>>>
>>>     store i8 %a, i8* %1
>>>
>>>     store i8 %b, i8* %2
>>>
>>>     %3 = load i8, i8* %1
>>>
>>>     %4 = zext i8 %3 to i32
>>>
>>>     %5 = load i8, i8* %2
>>>
>>>     %6 = zext i8 %5 to i32
>>>
>>>     %7 = add nsw i32 %4, %6
>>>
>>>     %8 = trunc i32 %7 to i8
>>>
>>>     ret i8 %8
>>>
>>> }
>>>
>>> zeroext b8 @sum(b8 zeroext %a, b8 zeroext %b) {
>>>
>>>     %1 = alloca b8
>>>
>>>     %2 = alloca b8
>>>
>>>     store b8 %a, b8* %1
>>>
>>>     store b8 %b, b8* %2
>>>
>>>     %3 = load b8, b8* %1
>>>
>>>     %4 = bytecast b8 %3 to i8
>>>
>>>     %5 = zext i8 %4 to i32
>>>
>>>     %6 = load b8, b8* %2
>>>
>>>     %7 = bytecast b8 %6 to i8
>>>
>>>     %8 = zext i8 %7 to i32
>>>
>>>     %9 = add nsw i32 %5, %8
>>>
>>>     %10 = trunc i32 %9 to b8
>>>
>>>     ret b8 %10
>>>
>>> }
>>>
>>>
>>>
>>>
>>>
>>> Thanks,
>>>
>>> George
>>>
>>>
>>> _______________________________________________
>>> cfe-dev mailing list
>>> cfe-dev at lists.llvm.org
>>> https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-dev
_______________________________________________
LLVM Developers mailing list
llvm-dev at lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev



More information about the llvm-dev mailing list